mito-ai 0.1.50__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 (205) hide show
  1. mito_ai/__init__.py +114 -0
  2. mito_ai/_version.py +4 -0
  3. mito_ai/anthropic_client.py +334 -0
  4. mito_ai/app_deploy/__init__.py +6 -0
  5. mito_ai/app_deploy/app_deploy_utils.py +44 -0
  6. mito_ai/app_deploy/handlers.py +345 -0
  7. mito_ai/app_deploy/models.py +98 -0
  8. mito_ai/app_manager/__init__.py +4 -0
  9. mito_ai/app_manager/handlers.py +167 -0
  10. mito_ai/app_manager/models.py +71 -0
  11. mito_ai/app_manager/utils.py +24 -0
  12. mito_ai/auth/README.md +18 -0
  13. mito_ai/auth/__init__.py +6 -0
  14. mito_ai/auth/handlers.py +96 -0
  15. mito_ai/auth/urls.py +13 -0
  16. mito_ai/chat_history/handlers.py +63 -0
  17. mito_ai/chat_history/urls.py +32 -0
  18. mito_ai/completions/completion_handlers/__init__.py +3 -0
  19. mito_ai/completions/completion_handlers/agent_auto_error_fixup_handler.py +59 -0
  20. mito_ai/completions/completion_handlers/agent_execution_handler.py +66 -0
  21. mito_ai/completions/completion_handlers/chat_completion_handler.py +141 -0
  22. mito_ai/completions/completion_handlers/code_explain_handler.py +113 -0
  23. mito_ai/completions/completion_handlers/completion_handler.py +42 -0
  24. mito_ai/completions/completion_handlers/inline_completer_handler.py +48 -0
  25. mito_ai/completions/completion_handlers/smart_debug_handler.py +160 -0
  26. mito_ai/completions/completion_handlers/utils.py +147 -0
  27. mito_ai/completions/handlers.py +415 -0
  28. mito_ai/completions/message_history.py +401 -0
  29. mito_ai/completions/models.py +404 -0
  30. mito_ai/completions/prompt_builders/__init__.py +3 -0
  31. mito_ai/completions/prompt_builders/agent_execution_prompt.py +57 -0
  32. mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +160 -0
  33. mito_ai/completions/prompt_builders/agent_system_message.py +472 -0
  34. mito_ai/completions/prompt_builders/chat_name_prompt.py +15 -0
  35. mito_ai/completions/prompt_builders/chat_prompt.py +116 -0
  36. mito_ai/completions/prompt_builders/chat_system_message.py +92 -0
  37. mito_ai/completions/prompt_builders/explain_code_prompt.py +32 -0
  38. mito_ai/completions/prompt_builders/inline_completer_prompt.py +197 -0
  39. mito_ai/completions/prompt_builders/prompt_constants.py +170 -0
  40. mito_ai/completions/prompt_builders/smart_debug_prompt.py +199 -0
  41. mito_ai/completions/prompt_builders/utils.py +84 -0
  42. mito_ai/completions/providers.py +284 -0
  43. mito_ai/constants.py +63 -0
  44. mito_ai/db/__init__.py +3 -0
  45. mito_ai/db/crawlers/__init__.py +6 -0
  46. mito_ai/db/crawlers/base_crawler.py +61 -0
  47. mito_ai/db/crawlers/constants.py +43 -0
  48. mito_ai/db/crawlers/snowflake.py +71 -0
  49. mito_ai/db/handlers.py +168 -0
  50. mito_ai/db/models.py +31 -0
  51. mito_ai/db/urls.py +34 -0
  52. mito_ai/db/utils.py +185 -0
  53. mito_ai/docker/mssql/compose.yml +37 -0
  54. mito_ai/docker/mssql/init/setup.sql +21 -0
  55. mito_ai/docker/mysql/compose.yml +18 -0
  56. mito_ai/docker/mysql/init/setup.sql +13 -0
  57. mito_ai/docker/oracle/compose.yml +17 -0
  58. mito_ai/docker/oracle/init/setup.sql +20 -0
  59. mito_ai/docker/postgres/compose.yml +17 -0
  60. mito_ai/docker/postgres/init/setup.sql +13 -0
  61. mito_ai/enterprise/__init__.py +3 -0
  62. mito_ai/enterprise/utils.py +15 -0
  63. mito_ai/file_uploads/__init__.py +3 -0
  64. mito_ai/file_uploads/handlers.py +248 -0
  65. mito_ai/file_uploads/urls.py +21 -0
  66. mito_ai/gemini_client.py +232 -0
  67. mito_ai/log/handlers.py +38 -0
  68. mito_ai/log/urls.py +21 -0
  69. mito_ai/logger.py +37 -0
  70. mito_ai/openai_client.py +382 -0
  71. mito_ai/path_utils.py +70 -0
  72. mito_ai/rules/handlers.py +44 -0
  73. mito_ai/rules/urls.py +22 -0
  74. mito_ai/rules/utils.py +56 -0
  75. mito_ai/settings/handlers.py +41 -0
  76. mito_ai/settings/urls.py +20 -0
  77. mito_ai/settings/utils.py +42 -0
  78. mito_ai/streamlit_conversion/agent_utils.py +37 -0
  79. mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
  80. mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
  81. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
  82. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
  83. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
  84. mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
  85. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  86. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  87. mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
  88. mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
  89. mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
  90. mito_ai/streamlit_preview/__init__.py +6 -0
  91. mito_ai/streamlit_preview/handlers.py +111 -0
  92. mito_ai/streamlit_preview/manager.py +152 -0
  93. mito_ai/streamlit_preview/urls.py +22 -0
  94. mito_ai/streamlit_preview/utils.py +29 -0
  95. mito_ai/tests/__init__.py +3 -0
  96. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  97. mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
  98. mito_ai/tests/conftest.py +53 -0
  99. mito_ai/tests/create_agent_system_message_prompt_test.py +22 -0
  100. mito_ai/tests/data/prompt_lg.py +69 -0
  101. mito_ai/tests/data/prompt_sm.py +6 -0
  102. mito_ai/tests/data/prompt_xl.py +13 -0
  103. mito_ai/tests/data/stock_data.sqlite3 +0 -0
  104. mito_ai/tests/db/conftest.py +39 -0
  105. mito_ai/tests/db/connections_test.py +102 -0
  106. mito_ai/tests/db/mssql_test.py +29 -0
  107. mito_ai/tests/db/mysql_test.py +29 -0
  108. mito_ai/tests/db/oracle_test.py +29 -0
  109. mito_ai/tests/db/postgres_test.py +29 -0
  110. mito_ai/tests/db/schema_test.py +93 -0
  111. mito_ai/tests/db/sqlite_test.py +31 -0
  112. mito_ai/tests/db/test_db_constants.py +61 -0
  113. mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
  114. mito_ai/tests/file_uploads/__init__.py +2 -0
  115. mito_ai/tests/file_uploads/test_handlers.py +282 -0
  116. mito_ai/tests/message_history/test_generate_short_chat_name.py +120 -0
  117. mito_ai/tests/message_history/test_message_history_utils.py +469 -0
  118. mito_ai/tests/open_ai_utils_test.py +152 -0
  119. mito_ai/tests/performance_test.py +329 -0
  120. mito_ai/tests/providers/test_anthropic_client.py +447 -0
  121. mito_ai/tests/providers/test_azure.py +631 -0
  122. mito_ai/tests/providers/test_capabilities.py +120 -0
  123. mito_ai/tests/providers/test_gemini_client.py +195 -0
  124. mito_ai/tests/providers/test_mito_server_utils.py +448 -0
  125. mito_ai/tests/providers/test_model_resolution.py +130 -0
  126. mito_ai/tests/providers/test_openai_client.py +57 -0
  127. mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
  128. mito_ai/tests/providers/test_provider_limits.py +42 -0
  129. mito_ai/tests/providers/test_providers.py +382 -0
  130. mito_ai/tests/providers/test_retry_logic.py +389 -0
  131. mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
  132. mito_ai/tests/providers/utils.py +85 -0
  133. mito_ai/tests/rules/conftest.py +26 -0
  134. mito_ai/tests/rules/rules_test.py +117 -0
  135. mito_ai/tests/server_limits_test.py +406 -0
  136. mito_ai/tests/settings/conftest.py +26 -0
  137. mito_ai/tests/settings/settings_test.py +70 -0
  138. mito_ai/tests/settings/test_settings_constants.py +9 -0
  139. mito_ai/tests/streamlit_conversion/__init__.py +3 -0
  140. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  141. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
  142. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
  143. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
  144. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
  145. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
  146. mito_ai/tests/test_constants.py +47 -0
  147. mito_ai/tests/test_telemetry.py +12 -0
  148. mito_ai/tests/user/__init__.py +2 -0
  149. mito_ai/tests/user/test_user.py +120 -0
  150. mito_ai/tests/utils/__init__.py +3 -0
  151. mito_ai/tests/utils/test_anthropic_utils.py +162 -0
  152. mito_ai/tests/utils/test_gemini_utils.py +98 -0
  153. mito_ai/tests/version_check_test.py +169 -0
  154. mito_ai/user/handlers.py +45 -0
  155. mito_ai/user/urls.py +21 -0
  156. mito_ai/utils/__init__.py +3 -0
  157. mito_ai/utils/anthropic_utils.py +168 -0
  158. mito_ai/utils/create.py +94 -0
  159. mito_ai/utils/db.py +74 -0
  160. mito_ai/utils/error_classes.py +42 -0
  161. mito_ai/utils/gemini_utils.py +133 -0
  162. mito_ai/utils/message_history_utils.py +87 -0
  163. mito_ai/utils/mito_server_utils.py +242 -0
  164. mito_ai/utils/open_ai_utils.py +200 -0
  165. mito_ai/utils/provider_utils.py +49 -0
  166. mito_ai/utils/schema.py +86 -0
  167. mito_ai/utils/server_limits.py +152 -0
  168. mito_ai/utils/telemetry_utils.py +480 -0
  169. mito_ai/utils/utils.py +89 -0
  170. mito_ai/utils/version_utils.py +94 -0
  171. mito_ai/utils/websocket_base.py +88 -0
  172. mito_ai/version_check.py +60 -0
  173. mito_ai-0.1.50.data/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +7 -0
  174. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/build_log.json +728 -0
  175. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/package.json +243 -0
  176. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +238 -0
  177. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +37 -0
  178. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +21602 -0
  179. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
  180. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
  181. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
  182. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js +619 -0
  183. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js.map +1 -0
  184. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style.js +4 -0
  185. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +712 -0
  186. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
  187. mito_ai-0.1.50.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 +533 -0
  188. mito_ai-0.1.50.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 +1 -0
  189. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +6941 -0
  190. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +1 -0
  191. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
  192. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
  193. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +59698 -0
  194. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
  195. mito_ai-0.1.50.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 +7440 -0
  196. mito_ai-0.1.50.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 +1 -0
  197. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2792 -0
  198. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
  199. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +4859 -0
  200. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +1 -0
  201. mito_ai-0.1.50.dist-info/METADATA +221 -0
  202. mito_ai-0.1.50.dist-info/RECORD +205 -0
  203. mito_ai-0.1.50.dist-info/WHEEL +4 -0
  204. mito_ai-0.1.50.dist-info/entry_points.txt +2 -0
  205. mito_ai-0.1.50.dist-info/licenses/LICENSE +3 -0
@@ -0,0 +1,631 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ from __future__ import annotations
5
+ from typing import Any, List
6
+ from unittest.mock import patch, MagicMock, AsyncMock
7
+
8
+ import pytest
9
+ from traitlets.config import Config
10
+ from openai.types.chat import ChatCompletionMessageParam
11
+
12
+ from mito_ai.completions.providers import OpenAIProvider
13
+ from mito_ai.completions.models import (
14
+ MessageType,
15
+ AICapabilities,
16
+ CompletionReply,
17
+ CompletionItem,
18
+ ResponseFormatInfo,
19
+ AgentResponse
20
+ )
21
+ from mito_ai.openai_client import OpenAIClient
22
+
23
+
24
+ FAKE_API_KEY = "sk-1234567890"
25
+ FAKE_AZURE_ENDPOINT = "https://test-azure-openai.openai.azure.com"
26
+ FAKE_AZURE_MODEL = "gpt-4o-azure"
27
+ FAKE_AZURE_API_VERSION = "2024-12-01-preview"
28
+
29
+
30
+ @pytest.fixture
31
+ def provider_config() -> Config:
32
+ """Create a proper Config object for the OpenAIProvider."""
33
+ config = Config()
34
+ config.OpenAIProvider = Config()
35
+ config.OpenAIClient = Config()
36
+ return config
37
+
38
+
39
+ @pytest.fixture(autouse=True)
40
+ def reset_env_vars(monkeypatch: pytest.MonkeyPatch) -> None:
41
+ """Reset all environment variables before each test."""
42
+ for var in [
43
+ "OPENAI_API_KEY", "CLAUDE_API_KEY", "GEMINI_API_KEY", "OLLAMA_MODEL",
44
+ "AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_MODEL",
45
+ "AZURE_OPENAI_API_VERSION"
46
+ ]:
47
+ monkeypatch.delenv(var, raising=False)
48
+
49
+
50
+ @pytest.fixture
51
+ def mock_azure_openai_environment(monkeypatch: pytest.MonkeyPatch) -> None:
52
+ """Set up Azure OpenAI environment variables and mocks."""
53
+ # Set environment variables
54
+ monkeypatch.setenv("AZURE_OPENAI_API_KEY", FAKE_API_KEY)
55
+ monkeypatch.setenv("AZURE_OPENAI_ENDPOINT", FAKE_AZURE_ENDPOINT)
56
+ monkeypatch.setenv("AZURE_OPENAI_MODEL", FAKE_AZURE_MODEL)
57
+ monkeypatch.setenv("AZURE_OPENAI_API_VERSION", FAKE_AZURE_API_VERSION)
58
+
59
+ # Set constants
60
+ monkeypatch.setattr("mito_ai.constants.AZURE_OPENAI_API_KEY", FAKE_API_KEY)
61
+ monkeypatch.setattr("mito_ai.constants.AZURE_OPENAI_ENDPOINT", FAKE_AZURE_ENDPOINT)
62
+ monkeypatch.setattr("mito_ai.constants.AZURE_OPENAI_MODEL", FAKE_AZURE_MODEL)
63
+ monkeypatch.setattr("mito_ai.constants.AZURE_OPENAI_API_VERSION", FAKE_AZURE_API_VERSION)
64
+
65
+ # Mock enterprise/private functions and directly mock is_azure_openai_configured
66
+ monkeypatch.setattr("mito_ai.enterprise.utils.is_enterprise", lambda: True)
67
+ monkeypatch.setattr("mito_ai.enterprise.utils.is_mitosheet_private", lambda: False)
68
+ monkeypatch.setattr("mito_ai.enterprise.utils.is_azure_openai_configured", lambda: True)
69
+ # Also patch where it's imported in the OpenAI client
70
+ monkeypatch.setattr("mito_ai.openai_client.is_azure_openai_configured", lambda: True)
71
+
72
+ # Ensure no other OpenAI key is set
73
+ monkeypatch.setattr("mito_ai.constants.OPENAI_API_KEY", None)
74
+
75
+
76
+ @pytest.fixture
77
+ def mock_azure_openai_client() -> Any:
78
+ """Mock Azure OpenAI client for testing."""
79
+ mock_client = MagicMock()
80
+ mock_client.chat.completions.create = AsyncMock()
81
+ mock_client.is_closed.return_value = False
82
+ return mock_client
83
+
84
+
85
+ # Test message types that should use Azure OpenAI
86
+ COMPLETION_MESSAGE_TYPES = [
87
+ MessageType.CHAT,
88
+ MessageType.SMART_DEBUG,
89
+ MessageType.CODE_EXPLAIN,
90
+ MessageType.AGENT_EXECUTION,
91
+ MessageType.AGENT_AUTO_ERROR_FIXUP,
92
+ MessageType.INLINE_COMPLETION,
93
+ MessageType.CHAT_NAME_GENERATION,
94
+ ]
95
+
96
+ # Common test data
97
+ TEST_MESSAGES: List[ChatCompletionMessageParam] = [
98
+ {"role": "user", "content": "Test message"}
99
+ ]
100
+
101
+ # Helper functions for common test patterns
102
+ def create_mock_azure_client_with_response(response_content: str = "Test Azure completion") -> tuple[MagicMock, MagicMock]:
103
+ """Create a mock Azure OpenAI client with a standard response."""
104
+ mock_response = MagicMock()
105
+ mock_response.choices = [MagicMock()]
106
+ mock_response.choices[0].message.content = response_content
107
+
108
+ mock_azure_client = MagicMock()
109
+ mock_azure_client.chat.completions.create = AsyncMock(return_value=mock_response)
110
+ mock_azure_client.is_closed.return_value = False
111
+
112
+ return mock_azure_client, mock_response
113
+
114
+ def create_mock_streaming_response(chunks: List[str]) -> Any:
115
+ """Create a mock streaming response with the given chunks."""
116
+ async def mock_stream():
117
+ for i, content in enumerate(chunks):
118
+ mock_chunk = MagicMock()
119
+ mock_chunk.choices = [MagicMock()]
120
+ mock_chunk.choices[0].delta.content = content
121
+ mock_chunk.choices[0].finish_reason = "stop" if i == len(chunks) - 1 else None
122
+ yield mock_chunk
123
+ return mock_stream
124
+
125
+ def assert_azure_client_called_correctly(mock_azure_client_class: MagicMock, mock_azure_client: MagicMock, expected_model: str = FAKE_AZURE_MODEL, should_stream: bool = False) -> None:
126
+ """Assert that Azure client was called correctly."""
127
+ # Verify Azure client was created
128
+ mock_azure_client_class.assert_called_once()
129
+
130
+ # Verify request was made through Azure client
131
+ mock_azure_client.chat.completions.create.assert_called_once()
132
+
133
+ # Verify the model used was the Azure model
134
+ call_args = mock_azure_client.chat.completions.create.call_args
135
+ assert call_args[1]["model"] == expected_model
136
+
137
+ if should_stream:
138
+ assert call_args[1]["stream"] == True
139
+
140
+
141
+ class TestAzureOpenAIClientCreation:
142
+ """Test that Azure OpenAI client is properly created when configured."""
143
+
144
+ def test_azure_openai_client_capabilities(self, mock_azure_openai_environment: None, provider_config: Config) -> None:
145
+ """Test that Azure OpenAI capabilities are properly returned."""
146
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client:
147
+ openai_client = OpenAIClient(config=provider_config)
148
+ capabilities = openai_client.capabilities
149
+
150
+ assert capabilities.provider == "Azure OpenAI"
151
+ assert capabilities.configuration["model"] == FAKE_AZURE_MODEL
152
+
153
+ # Access the client to trigger creation
154
+ # This let's us test that building the client works
155
+ _ = openai_client._active_async_client
156
+ mock_azure_client.assert_called_once()
157
+
158
+ def test_azure_openai_client_creation_parameters(self, mock_azure_openai_environment: None, provider_config: Config) -> None:
159
+ """Test that Azure OpenAI client is created with correct parameters."""
160
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client:
161
+ openai_client = OpenAIClient(config=provider_config)
162
+ # Access the client to trigger creation
163
+ _ = openai_client._active_async_client
164
+
165
+ mock_azure_client.assert_called_once_with(
166
+ api_key=FAKE_API_KEY,
167
+ api_version=FAKE_AZURE_API_VERSION,
168
+ azure_endpoint=FAKE_AZURE_ENDPOINT,
169
+ max_retries=1,
170
+ timeout=30,
171
+ )
172
+
173
+ def test_azure_openai_model_resolution(self, mock_azure_openai_environment: None, provider_config: Config) -> None:
174
+ """Test that Azure OpenAI model is used regardless of requested model."""
175
+ with patch("openai.AsyncAzureOpenAI"):
176
+ openai_client = OpenAIClient(config=provider_config)
177
+
178
+ # Test with gpt-4.1 model
179
+ resolved_model = openai_client._adjust_model_for_azure_or_ollama("gpt-4.1")
180
+ assert resolved_model == FAKE_AZURE_MODEL
181
+
182
+ # Test with any other model
183
+ resolved_model = openai_client._adjust_model_for_azure_or_ollama("gpt-3.5-turbo")
184
+ assert resolved_model == FAKE_AZURE_MODEL
185
+
186
+
187
+ class TestAzureOpenAICompletions:
188
+ """Test Azure OpenAI request_completions method."""
189
+
190
+ @pytest.mark.asyncio
191
+ @pytest.mark.parametrize("message_type", COMPLETION_MESSAGE_TYPES)
192
+ async def test_request_completions_uses_azure_client(
193
+ self,
194
+ mock_azure_openai_environment: None,
195
+ provider_config: Config,
196
+ message_type: MessageType
197
+ ) -> None:
198
+ """Test that request_completions uses Azure OpenAI client for all message types."""
199
+
200
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client_class:
201
+ mock_azure_client, mock_response = create_mock_azure_client_with_response()
202
+ mock_azure_client_class.return_value = mock_azure_client
203
+
204
+ openai_client = OpenAIClient(config=provider_config)
205
+
206
+ completion = await openai_client.request_completions(
207
+ message_type=message_type,
208
+ messages=TEST_MESSAGES,
209
+ model="gpt-4.1"
210
+ )
211
+
212
+ # Verify the completion was returned
213
+ assert completion == "Test Azure completion"
214
+
215
+ # Verify Azure client was called correctly
216
+ assert_azure_client_called_correctly(mock_azure_client_class, mock_azure_client)
217
+
218
+ @pytest.mark.asyncio
219
+ async def test_request_completions_with_response_format(
220
+ self,
221
+ mock_azure_openai_environment: None,
222
+ provider_config: Config
223
+ ) -> None:
224
+ """Test that request_completions works with response format (agent mode)."""
225
+
226
+ # Mock the response
227
+ mock_response = MagicMock()
228
+ mock_response.choices = [MagicMock()]
229
+ mock_response.choices[0].message.content = '{"type": "finished_task", "message": "Task completed"}'
230
+
231
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client_class:
232
+ mock_azure_client = MagicMock()
233
+ mock_azure_client.chat.completions.create = AsyncMock(return_value=mock_response)
234
+ mock_azure_client.is_closed.return_value = False
235
+ mock_azure_client_class.return_value = mock_azure_client
236
+
237
+ openai_client = OpenAIClient(config=provider_config)
238
+
239
+ messages: List[ChatCompletionMessageParam] = [
240
+ {"role": "user", "content": "Test message"}
241
+ ]
242
+
243
+ response_format_info = ResponseFormatInfo(
244
+ name="agent_response",
245
+ format=AgentResponse
246
+ )
247
+
248
+ completion = await openai_client.request_completions(
249
+ message_type=MessageType.AGENT_EXECUTION,
250
+ messages=messages,
251
+ model="gpt-4.1",
252
+ response_format_info=response_format_info
253
+ )
254
+
255
+ # Verify the completion was returned
256
+ assert completion == '{"type": "finished_task", "message": "Task completed"}'
257
+
258
+ # Verify Azure client was used
259
+ mock_azure_client.chat.completions.create.assert_called_once()
260
+
261
+ # Verify the model used was the Azure model
262
+ call_args = mock_azure_client.chat.completions.create.call_args
263
+ assert call_args[1]["model"] == FAKE_AZURE_MODEL
264
+
265
+ @pytest.mark.asyncio
266
+ @pytest.mark.parametrize("requested_model", ["gpt-4.1", "gpt-3.5-turbo", "gpt-4-turbo", "gpt-4o"])
267
+ async def test_request_completions_uses_azure_model_not_requested_model(
268
+ self,
269
+ mock_azure_openai_environment: None,
270
+ provider_config: Config,
271
+ requested_model: str
272
+ ) -> None:
273
+ """Test that Azure model is used regardless of requested model when Azure is configured."""
274
+
275
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client_class:
276
+ mock_azure_client, mock_response = create_mock_azure_client_with_response()
277
+ mock_azure_client_class.return_value = mock_azure_client
278
+
279
+ openai_client = OpenAIClient(config=provider_config)
280
+
281
+ completion = await openai_client.request_completions(
282
+ message_type=MessageType.CHAT,
283
+ messages=TEST_MESSAGES,
284
+ model=requested_model
285
+ )
286
+
287
+ assert completion == "Test Azure completion"
288
+
289
+ # Verify Azure client was called correctly and used Azure model, not requested model
290
+ assert_azure_client_called_correctly(mock_azure_client_class, mock_azure_client)
291
+ call_args = mock_azure_client.chat.completions.create.call_args
292
+ assert call_args[1]["model"] != requested_model # Explicitly check it's not the requested model
293
+
294
+
295
+ class TestAzureOpenAIStreamCompletions:
296
+ """Test Azure OpenAI stream_completions method."""
297
+
298
+ @pytest.mark.asyncio
299
+ @pytest.mark.parametrize("message_type", COMPLETION_MESSAGE_TYPES)
300
+ async def test_stream_completions_uses_azure_client(
301
+ self,
302
+ mock_azure_openai_environment: None,
303
+ provider_config: Config,
304
+ message_type: MessageType
305
+ ) -> None:
306
+ """Test that stream_completions uses Azure OpenAI client for all message types."""
307
+
308
+ stream_chunks = ["Hello", " World"]
309
+ expected_completion = "".join(stream_chunks)
310
+
311
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client_class:
312
+ mock_azure_client = MagicMock()
313
+ mock_azure_client.chat.completions.create = AsyncMock(return_value=create_mock_streaming_response(stream_chunks)())
314
+ mock_azure_client.is_closed.return_value = False
315
+ mock_azure_client_class.return_value = mock_azure_client
316
+
317
+ openai_client = OpenAIClient(config=provider_config)
318
+
319
+ reply_chunks = []
320
+ def mock_reply(chunk):
321
+ reply_chunks.append(chunk)
322
+
323
+ completion = await openai_client.stream_completions(
324
+ message_type=message_type,
325
+ messages=TEST_MESSAGES,
326
+ model="gpt-4.1",
327
+ message_id="test-id",
328
+ thread_id="test-thread",
329
+ reply_fn=mock_reply
330
+ )
331
+
332
+ # Verify the full completion was returned
333
+ assert completion == expected_completion
334
+
335
+ # Verify Azure client was called correctly for streaming
336
+ assert_azure_client_called_correctly(mock_azure_client_class, mock_azure_client, should_stream=True)
337
+
338
+ # Verify reply function was called with chunks
339
+ assert len(reply_chunks) >= 2 # Initial reply + chunks
340
+ assert isinstance(reply_chunks[0], CompletionReply)
341
+
342
+ @pytest.mark.asyncio
343
+ async def test_stream_completions_with_response_format(
344
+ self,
345
+ mock_azure_openai_environment: None,
346
+ provider_config: Config
347
+ ) -> None:
348
+ """Test that stream_completions works with response format (agent mode)."""
349
+
350
+ # Mock the streaming response
351
+ mock_chunk1 = MagicMock()
352
+ mock_chunk1.choices = [MagicMock()]
353
+ mock_chunk1.choices[0].delta.content = '{"type": "finished_task",'
354
+ mock_chunk1.choices[0].finish_reason = None
355
+
356
+ mock_chunk2 = MagicMock()
357
+ mock_chunk2.choices = [MagicMock()]
358
+ mock_chunk2.choices[0].delta.content = ' "message": "Task completed"}'
359
+ mock_chunk2.choices[0].finish_reason = "stop"
360
+
361
+ async def mock_stream():
362
+ yield mock_chunk1
363
+ yield mock_chunk2
364
+
365
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client_class:
366
+ mock_azure_client = MagicMock()
367
+ mock_azure_client.chat.completions.create = AsyncMock(return_value=mock_stream())
368
+ mock_azure_client.is_closed.return_value = False
369
+ mock_azure_client_class.return_value = mock_azure_client
370
+
371
+ openai_client = OpenAIClient(config=provider_config)
372
+
373
+ messages: List[ChatCompletionMessageParam] = [
374
+ {"role": "user", "content": "Test message"}
375
+ ]
376
+
377
+ response_format_info = ResponseFormatInfo(
378
+ name="agent_response",
379
+ format=AgentResponse
380
+ )
381
+
382
+ reply_chunks = []
383
+ def mock_reply(chunk):
384
+ reply_chunks.append(chunk)
385
+
386
+ completion = await openai_client.stream_completions(
387
+ message_type=MessageType.AGENT_EXECUTION,
388
+ messages=messages,
389
+ model="gpt-4.1",
390
+ message_id="test-id",
391
+ thread_id="test-thread",
392
+ reply_fn=mock_reply,
393
+ response_format_info=response_format_info
394
+ )
395
+
396
+ # Verify the full completion was returned
397
+ assert completion == '{"type": "finished_task", "message": "Task completed"}'
398
+
399
+ # Verify Azure client was used
400
+ mock_azure_client.chat.completions.create.assert_called_once()
401
+
402
+ # Verify the model used was the Azure model
403
+ call_args = mock_azure_client.chat.completions.create.call_args
404
+ assert call_args[1]["model"] == FAKE_AZURE_MODEL
405
+
406
+
407
+ class TestAzureOpenAIProviderIntegration:
408
+ """Test Azure OpenAI integration through the OpenAIProvider."""
409
+
410
+ @pytest.mark.asyncio
411
+ @pytest.mark.parametrize("message_type", COMPLETION_MESSAGE_TYPES)
412
+ async def test_provider_uses_azure_for_gpt_4_1(
413
+ self,
414
+ mock_azure_openai_environment: None,
415
+ provider_config: Config,
416
+ message_type: MessageType
417
+ ) -> None:
418
+ """Test that OpenAIProvider uses Azure OpenAI when gpt-4.1 is requested and Azure is configured."""
419
+
420
+ # Mock the response
421
+ mock_response = MagicMock()
422
+ mock_response.choices = [MagicMock()]
423
+ mock_response.choices[0].message.content = "Test Azure completion"
424
+
425
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client_class:
426
+ mock_azure_client = MagicMock()
427
+ mock_azure_client.chat.completions.create = AsyncMock(return_value=mock_response)
428
+ mock_azure_client.is_closed.return_value = False
429
+ mock_azure_client_class.return_value = mock_azure_client
430
+
431
+ provider = OpenAIProvider(config=provider_config)
432
+
433
+ messages: List[ChatCompletionMessageParam] = [
434
+ {"role": "user", "content": "Test message"}
435
+ ]
436
+
437
+ completion = await provider.request_completions(
438
+ message_type=message_type,
439
+ messages=messages,
440
+ model="gpt-4.1"
441
+ )
442
+
443
+ # Verify the completion was returned
444
+ assert completion == "Test Azure completion"
445
+
446
+ # Verify Azure client was created
447
+ mock_azure_client_class.assert_called_once()
448
+
449
+ # Verify request was made through Azure client
450
+ mock_azure_client.chat.completions.create.assert_called_once()
451
+
452
+ # Verify the model used was the Azure model, not the requested model
453
+ call_args = mock_azure_client.chat.completions.create.call_args
454
+ assert call_args[1]["model"] == FAKE_AZURE_MODEL
455
+
456
+ @pytest.mark.asyncio
457
+ @pytest.mark.parametrize("message_type", COMPLETION_MESSAGE_TYPES)
458
+ async def test_provider_stream_uses_azure_for_gpt_4_1(
459
+ self,
460
+ mock_azure_openai_environment: None,
461
+ provider_config: Config,
462
+ message_type: MessageType
463
+ ) -> None:
464
+ """Test that OpenAIProvider stream_completions uses Azure OpenAI when gpt-4.1 is requested and Azure is configured."""
465
+
466
+ # Mock the streaming response
467
+ mock_chunk1 = MagicMock()
468
+ mock_chunk1.choices = [MagicMock()]
469
+ mock_chunk1.choices[0].delta.content = "Hello"
470
+ mock_chunk1.choices[0].finish_reason = None
471
+
472
+ mock_chunk2 = MagicMock()
473
+ mock_chunk2.choices = [MagicMock()]
474
+ mock_chunk2.choices[0].delta.content = " Azure"
475
+ mock_chunk2.choices[0].finish_reason = "stop"
476
+
477
+ async def mock_stream():
478
+ yield mock_chunk1
479
+ yield mock_chunk2
480
+
481
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client_class:
482
+ mock_azure_client = MagicMock()
483
+ mock_azure_client.chat.completions.create = AsyncMock(return_value=mock_stream())
484
+ mock_azure_client.is_closed.return_value = False
485
+ mock_azure_client_class.return_value = mock_azure_client
486
+
487
+ provider = OpenAIProvider(config=provider_config)
488
+
489
+ messages: List[ChatCompletionMessageParam] = [
490
+ {"role": "user", "content": "Test message"}
491
+ ]
492
+
493
+ reply_chunks = []
494
+ def mock_reply(chunk):
495
+ reply_chunks.append(chunk)
496
+
497
+ completion = await provider.stream_completions(
498
+ message_type=message_type,
499
+ messages=messages,
500
+ model="gpt-4.1",
501
+ message_id="test-id",
502
+ thread_id="test-thread",
503
+ reply_fn=mock_reply
504
+ )
505
+
506
+ # Verify the full completion was returned
507
+ assert completion == "Hello Azure"
508
+
509
+ # Verify Azure client was created
510
+ mock_azure_client_class.assert_called_once()
511
+
512
+ # Verify request was made through Azure client
513
+ mock_azure_client.chat.completions.create.assert_called_once()
514
+
515
+ # Verify the model used was the Azure model, not the requested model
516
+ call_args = mock_azure_client.chat.completions.create.call_args
517
+ assert call_args[1]["model"] == FAKE_AZURE_MODEL
518
+ assert call_args[1]["stream"] == True
519
+
520
+
521
+ class TestAzureOpenAIConfigurationPriority:
522
+ """Test that Azure OpenAI is used when configured, regardless of other providers."""
523
+
524
+ def test_azure_openai_priority_over_regular_openai(
525
+ self,
526
+ mock_azure_openai_environment: None,
527
+ provider_config: Config,
528
+ monkeypatch: pytest.MonkeyPatch
529
+ ) -> None:
530
+ """Test that Azure OpenAI is used even when regular OpenAI key is available."""
531
+
532
+ # Set regular OpenAI key (this should be overridden by Azure OpenAI)
533
+ monkeypatch.setenv("OPENAI_API_KEY", "sk-regular-openai-key")
534
+ monkeypatch.setattr("mito_ai.constants.OPENAI_API_KEY", "sk-regular-openai-key")
535
+
536
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client:
537
+ openai_client = OpenAIClient(config=provider_config)
538
+ capabilities = openai_client.capabilities
539
+
540
+ # Should still use Azure OpenAI, not regular OpenAI
541
+ assert capabilities.provider == "Azure OpenAI"
542
+ assert capabilities.configuration["model"] == FAKE_AZURE_MODEL
543
+
544
+ # Access the client to trigger creation
545
+ _ = openai_client._active_async_client
546
+ mock_azure_client.assert_called_once()
547
+
548
+ def test_azure_openai_priority_over_claude(
549
+ self,
550
+ mock_azure_openai_environment: None,
551
+ provider_config: Config,
552
+ monkeypatch: pytest.MonkeyPatch
553
+ ) -> None:
554
+ """Test that Azure OpenAI is used even when Claude key is available."""
555
+
556
+ # Set Claude key (this should be overridden by Azure OpenAI)
557
+ monkeypatch.setenv("CLAUDE_API_KEY", "claude-key")
558
+ monkeypatch.setattr("mito_ai.constants.CLAUDE_API_KEY", "claude-key")
559
+
560
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client:
561
+ openai_client = OpenAIClient(config=provider_config)
562
+ capabilities = openai_client.capabilities
563
+
564
+ # Should still use Azure OpenAI, not Claude
565
+ assert capabilities.provider == "Azure OpenAI"
566
+ assert capabilities.configuration["model"] == FAKE_AZURE_MODEL
567
+
568
+ # Access the client to trigger creation
569
+ _ = openai_client._active_async_client
570
+ mock_azure_client.assert_called_once()
571
+
572
+
573
+ class TestAzureOpenAINotConfigured:
574
+ """Test behavior when Azure OpenAI is not properly configured."""
575
+
576
+ def test_missing_azure_api_key(self, provider_config: Config, monkeypatch: pytest.MonkeyPatch) -> None:
577
+ """Test that Azure OpenAI is not used when API key is missing."""
578
+
579
+ # Set some but not all Azure OpenAI env vars
580
+ monkeypatch.setenv("AZURE_OPENAI_ENDPOINT", FAKE_AZURE_ENDPOINT)
581
+ monkeypatch.setenv("AZURE_OPENAI_MODEL", FAKE_AZURE_MODEL)
582
+ monkeypatch.setenv("AZURE_OPENAI_API_VERSION", FAKE_AZURE_API_VERSION)
583
+ # Missing AZURE_OPENAI_API_KEY
584
+
585
+ monkeypatch.setattr("mito_ai.constants.AZURE_OPENAI_ENDPOINT", FAKE_AZURE_ENDPOINT)
586
+ monkeypatch.setattr("mito_ai.constants.AZURE_OPENAI_MODEL", FAKE_AZURE_MODEL)
587
+ monkeypatch.setattr("mito_ai.constants.AZURE_OPENAI_API_VERSION", FAKE_AZURE_API_VERSION)
588
+ monkeypatch.setattr("mito_ai.constants.AZURE_OPENAI_API_KEY", None)
589
+
590
+ monkeypatch.setattr("mito_ai.enterprise.utils.is_enterprise", lambda: True)
591
+ monkeypatch.setattr("mito_ai.enterprise.utils.is_mitosheet_private", lambda: False)
592
+ # This should return False due to missing API key
593
+ monkeypatch.setattr("mito_ai.enterprise.utils.is_azure_openai_configured", lambda: False)
594
+ monkeypatch.setattr("mito_ai.openai_client.is_azure_openai_configured", lambda: False)
595
+
596
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client:
597
+ openai_client = OpenAIClient(config=provider_config)
598
+ capabilities = openai_client.capabilities
599
+
600
+ # Should not use Azure OpenAI
601
+ assert capabilities.provider != "Azure OpenAI"
602
+ mock_azure_client.assert_not_called()
603
+
604
+ def test_not_enterprise_user(self, provider_config: Config, monkeypatch: pytest.MonkeyPatch) -> None:
605
+ """Test that Azure OpenAI is not used when user is not enterprise."""
606
+
607
+ # Set all Azure OpenAI env vars
608
+ monkeypatch.setenv("AZURE_OPENAI_API_KEY", FAKE_API_KEY)
609
+ monkeypatch.setenv("AZURE_OPENAI_ENDPOINT", FAKE_AZURE_ENDPOINT)
610
+ monkeypatch.setenv("AZURE_OPENAI_MODEL", FAKE_AZURE_MODEL)
611
+ monkeypatch.setenv("AZURE_OPENAI_API_VERSION", FAKE_AZURE_API_VERSION)
612
+
613
+ monkeypatch.setattr("mito_ai.constants.AZURE_OPENAI_API_KEY", FAKE_API_KEY)
614
+ monkeypatch.setattr("mito_ai.constants.AZURE_OPENAI_ENDPOINT", FAKE_AZURE_ENDPOINT)
615
+ monkeypatch.setattr("mito_ai.constants.AZURE_OPENAI_MODEL", FAKE_AZURE_MODEL)
616
+ monkeypatch.setattr("mito_ai.constants.AZURE_OPENAI_API_VERSION", FAKE_AZURE_API_VERSION)
617
+
618
+ # Not enterprise user
619
+ monkeypatch.setattr("mito_ai.enterprise.utils.is_enterprise", lambda: False)
620
+ monkeypatch.setattr("mito_ai.enterprise.utils.is_mitosheet_private", lambda: False)
621
+ # This should return False due to not being enterprise
622
+ monkeypatch.setattr("mito_ai.enterprise.utils.is_azure_openai_configured", lambda: False)
623
+ monkeypatch.setattr("mito_ai.openai_client.is_azure_openai_configured", lambda: False)
624
+
625
+ with patch("openai.AsyncAzureOpenAI") as mock_azure_client:
626
+ openai_client = OpenAIClient(config=provider_config)
627
+ capabilities = openai_client.capabilities
628
+
629
+ # Should not use Azure OpenAI
630
+ assert capabilities.provider != "Azure OpenAI"
631
+ mock_azure_client.assert_not_called()