Skip to content

Commit bacdef6

Browse files
authored
Merge pull request #2157 from mito-ds/dev
Release Jan 23, 2026
2 parents 3aa79da + 1f1c301 commit bacdef6

File tree

83 files changed

+3811
-908
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+3811
-908
lines changed

.cursor/commands/verify.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,24 @@ After each interaction:
4242

4343
### 7. Rebuild and Re-test
4444

45-
After making fixes:
46-
47-
1. **Setup Environment**:
48-
- For frontend changes: Wait a few seconds for rebuild to complete
49-
- For backend changes: Restart and relaunch the server
50-
51-
2. **Refresh browser**:
52-
- Take new snapshot
45+
After making code changes and before testing again:
46+
47+
1. **For Frontend Changes** (TypeScript/React/CSS/JavaScript):
48+
- Wait a few seconds for the build to complete (check the TypeScript terminal)
49+
- **Refresh the browser** to load the updated code:
50+
- Use browser navigation to reload the page, or
51+
- Use browser refresh functionality
52+
- Retest the feature
53+
54+
55+
2. **For Backend Changes** (Python/server code):
56+
- **Shut down the Jupyter server** (stop the running JupyterLab process)
57+
- **Relaunch the Jupyter server** to load the updated backend code
58+
- Navigate to the JupyterLab URL again
59+
- Wait 3-5 seconds for full page load
60+
- Take a new snapshot
5361
- Re-test the feature
54-
- Verify fix worked
5562

5663
### 8. Iterate Until Complete
5764

58-
Repeat steps 4-7 until the feature works correctly.
65+
Repeat steps 4-7 until the feature works correctly.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: Test - Mito AI Frontend Playwright with LiteLLM
2+
3+
on:
4+
push:
5+
branches: [ dev ]
6+
paths:
7+
- 'mito-ai/**'
8+
- 'tests/llm_providers_tests/litellm_llm_providers.spec.ts'
9+
- '.github/workflows/test-litellm-llm-providers.yml'
10+
pull_request:
11+
paths:
12+
- 'mito-ai/**'
13+
- 'tests/llm_providers_tests/litellm_llm_providers.spec.ts'
14+
- '.github/workflows/test-litellm-llm-providers.yml'
15+
workflow_dispatch:
16+
17+
concurrency:
18+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
19+
cancel-in-progress: true
20+
21+
jobs:
22+
test-mitoai-frontend-jupyterlab-litellm:
23+
runs-on: ubuntu-24.04
24+
timeout-minutes: 60
25+
strategy:
26+
matrix:
27+
python-version: ['3.10', '3.12']
28+
fail-fast: false
29+
30+
steps:
31+
- uses: actions/checkout@v4
32+
- name: Set up Python ${{ matrix.python-version }}
33+
uses: actions/setup-python@v5
34+
with:
35+
python-version: ${{ matrix.python-version }}
36+
cache: pip
37+
cache-dependency-path: |
38+
mito-ai/setup.py
39+
tests/requirements.txt
40+
- uses: actions/setup-node@v4
41+
with:
42+
node-version: 22
43+
cache: 'npm'
44+
cache-dependency-path: mito-ai/package-lock.json
45+
- name: Upgrade pip
46+
run: |
47+
python -m pip install --upgrade pip
48+
- name: Install dependencies
49+
run: |
50+
cd tests
51+
bash mac-setup.sh
52+
- name: Install mitosheet-helper-enterprise
53+
run: |
54+
cd tests
55+
source venv/bin/activate
56+
pip install mitosheet-helper-enterprise
57+
- name: Install JupyterLab
58+
run: |
59+
python -m pip install jupyterlab
60+
- name: Install Node.js dependencies
61+
run: |
62+
cd mito-ai
63+
jlpm install
64+
- name: Setup JupyterLab
65+
run: |
66+
cd tests
67+
source venv/bin/activate
68+
pip install setuptools==68.0.0
69+
cd ../mito-ai
70+
jupyter labextension develop . --overwrite
71+
jupyter server extension enable --py mito_ai
72+
- name: Start a server and run LiteLLM provider tests
73+
run: |
74+
cd tests
75+
source venv/bin/activate
76+
jupyter lab --config jupyter_server_test_config.py &
77+
jlpm run test:litellm-llm-providers
78+
env:
79+
LITELLM_BASE_URL: ${{ secrets.LITELLM_BASE_URL }}
80+
LITELLM_MODELS: ${{ secrets.LITELLM_MODELS }}
81+
LITELLM_API_KEY: ${{ secrets.LITELLM_API_KEY }}
82+
- name: Upload test-results
83+
uses: actions/upload-artifact@v4
84+
if: failure()
85+
with:
86+
name: mitoai-jupyterlab-playwright-litellm-report-${{ matrix.python-version }}-${{ github.run_id }}
87+
path: tests/playwright-report/
88+
retention-days: 14

mito-ai/.eslintignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
node_modules
2+
venv
3+
dist
4+
coverage
5+
**/*.d.ts
6+
tests
7+
**/__tests__
8+
ui-tests
9+
lib
10+
buildcache
11+
*.tsbuildinfo

mito-ai/docs/litellm-deployment.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# LiteLLM Enterprise Deployment Guide
2+
3+
This guide explains how to configure Mito AI for enterprise deployments with strict data privacy and security requirements.
4+
5+
## Overview
6+
7+
Enterprise mode in Mito AI provides:
8+
9+
1. **LLM Model Lockdown**: AI calls ONLY go to IT-approved LLM models
10+
2. **Telemetry Elimination**: No telemetry is sent to Mito servers
11+
3. **User Protection**: End users cannot change to unapproved LLM models
12+
13+
## Enabling Enterprise Mode
14+
15+
Enterprise mode is automatically enabled when the `mitosheet-helper-enterprise` package is installed. This package must be installed by your IT team with appropriate permissions.
16+
17+
```bash
18+
pip install mitosheet-helper-enterprise
19+
```
20+
21+
## LiteLLM Configuration
22+
23+
When enterprise mode is enabled, you can optionally configure LiteLLM to route all AI calls to your approved LLM endpoint. LiteLLM configuration is **optional** - if not configured, users can continue using the normal Mito server flow.
24+
25+
### Environment Variables
26+
27+
Configure the following environment variables on the Jupyter server:
28+
29+
#### IT-Controlled Variables (Set by IT Team)
30+
31+
- **`LITELLM_BASE_URL`**: The base URL of your LiteLLM server endpoint
32+
- Example: `https://your-litellm-server.com`
33+
- Must be OpenAI-compatible
34+
35+
- **`LITELLM_MODELS`**: Comma-separated list of approved model names
36+
- Model names must include provider prefix (e.g., `"openai/gpt-4o"`)
37+
- Example: `"openai/gpt-4o,openai/gpt-4o-mini,anthropic/claude-3-5-sonnet"`
38+
- Format: Comma-separated string (whitespace is automatically trimmed)
39+
- The first model in the list is the default model.
40+
41+
#### User-Controlled Variables (Set by Each End User)
42+
43+
- **`LITELLM_API_KEY`**: User's API key for authentication with the LiteLLM server
44+
- Each user sets their own API key
45+
- Keys are never sent to Mito servers
46+
47+
## Security Guarantees
48+
49+
1. **Defense in Depth**:
50+
- Backend validates all model selections (even if frontend is bypassed)
51+
- Frontend UI only shows approved models
52+
- All API calls go to LiteLLM base URL
53+
- If user does not set correct API key, the app will still not send requests to the Mito server, instead it will just show an error message.
54+
55+
56+
2. **Telemetry Elimination**:
57+
- Early return in telemetry functions when enterprise mode is active
58+
- No analytics library calls made
59+
- No network requests to external telemetry servers
60+
61+
3. **Model Lockdown**:
62+
- Backend validates all model selections against approved list
63+
- Backend rejects model change requests for unapproved models
64+
- Frontend shows only approved models in model selector
65+
66+
4. **API Key Management**:
67+
- Users set their own `LITELLM_API_KEY` environment variable for authentication
68+
- IT controls the LiteLLM endpoint and approved models, users control authentication
69+
- Keys never sent to Mito servers
70+
71+
## Verification
72+
73+
### Check Enterprise Mode Status
74+
75+
When you start Jupyter Lab, check the server logs for:
76+
77+
```
78+
Enterprise mode enabled
79+
LiteLLM configured: endpoint=https://your-litellm-server.com, models=['openai/gpt-4o', 'openai/gpt-4o-mini']
80+
```
81+
82+
### Verify Model Selection
83+
84+
1. Open Mito AI chat in Jupyter Lab
85+
2. Click on the model selector
86+
3. Verify only approved models from `LITELLM_MODELS` are displayed
87+
4. Verify you cannot select unapproved models

mito-ai/mito_ai/__init__.py

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
from typing import List, Dict
55
from jupyter_server.utils import url_path_join
66
from mito_ai.completions.handlers import CompletionHandler
7-
from mito_ai.completions.providers import OpenAIProvider
7+
from mito_ai.provider_manager import ProviderManager
88
from mito_ai.completions.message_history import GlobalMessageHistory
99
from mito_ai.app_deploy.handlers import AppDeployHandler
10-
from mito_ai.streamlit_preview.handlers import StreamlitPreviewHandler
1110
from mito_ai.log.urls import get_log_urls
11+
from mito_ai.utils.litellm_utils import is_litellm_configured
1212
from mito_ai.version_check import VersionCheckHandler
1313
from mito_ai.db.urls import get_db_urls
1414
from mito_ai.settings.urls import get_settings_urls
@@ -20,6 +20,8 @@
2020
from mito_ai.user.urls import get_user_urls
2121
from mito_ai.chat_history.urls import get_chat_history_urls
2222
from mito_ai.chart_wizard.urls import get_chart_wizard_urls
23+
from mito_ai.utils.version_utils import is_enterprise
24+
from mito_ai import constants
2325

2426
# Force Matplotlib to use the Jupyter inline backend.
2527
# Background: importing Streamlit sets os.environ["MPLBACKEND"] = "Agg" very early.
@@ -33,16 +35,6 @@
3335
import os
3436
os.environ["MPLBACKEND"] = "module://matplotlib_inline.backend_inline"
3537

36-
try:
37-
from _version import __version__
38-
except ImportError:
39-
# Fallback when using the package in dev mode without installing in editable mode with pip. It is highly recommended to install
40-
# the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs
41-
import warnings
42-
43-
warnings.warn("Importing 'mito_ai' outside a proper installation.")
44-
__version__ = "dev"
45-
4638
def _jupyter_labextension_paths() -> List[Dict[str, str]]:
4739
return [{"src": "labextension", "dest": "mito_ai"}]
4840

@@ -65,7 +57,7 @@ def _load_jupyter_server_extension(server_app) -> None: # type: ignore
6557
web_app = server_app.web_app
6658
base_url = web_app.settings["base_url"]
6759

68-
open_ai_provider = OpenAIProvider(config=server_app.config)
60+
provider_manager = ProviderManager(config=server_app.config)
6961

7062
# Create a single GlobalMessageHistory instance for the entire server
7163
# This ensures thread-safe access to the .mito/ai-chats directory
@@ -76,18 +68,13 @@ def _load_jupyter_server_extension(server_app) -> None: # type: ignore
7668
(
7769
url_path_join(base_url, "mito-ai", "completions"),
7870
CompletionHandler,
79-
{"llm": open_ai_provider, "message_history": global_message_history},
71+
{"llm": provider_manager, "message_history": global_message_history},
8072
),
8173
(
8274
url_path_join(base_url, "mito-ai", "app-deploy"),
8375
AppDeployHandler,
8476
{}
8577
),
86-
(
87-
url_path_join(base_url, "mito-ai", "streamlit-preview"),
88-
StreamlitPreviewHandler,
89-
{}
90-
),
9178
(
9279
url_path_join(base_url, "mito-ai", "version-check"),
9380
VersionCheckHandler,
@@ -104,13 +91,20 @@ def _load_jupyter_server_extension(server_app) -> None: # type: ignore
10491
handlers.extend(get_db_urls(base_url)) # type: ignore
10592
handlers.extend(get_settings_urls(base_url)) # type: ignore
10693
handlers.extend(get_rules_urls(base_url)) # type: ignore
107-
handlers.extend(get_log_urls(base_url, open_ai_provider.key_type)) # type: ignore
94+
handlers.extend(get_log_urls(base_url, provider_manager.key_type)) # type: ignore
10895
handlers.extend(get_auth_urls(base_url)) # type: ignore
109-
handlers.extend(get_streamlit_preview_urls(base_url)) # type: ignore
96+
handlers.extend(get_streamlit_preview_urls(base_url, provider_manager)) # type: ignore
11097
handlers.extend(get_file_uploads_urls(base_url)) # type: ignore
11198
handlers.extend(get_user_urls(base_url)) # type: ignore
11299
handlers.extend(get_chat_history_urls(base_url, global_message_history)) # type: ignore
113-
handlers.extend(get_chart_wizard_urls(base_url, open_ai_provider)) # type: ignore
100+
handlers.extend(get_chart_wizard_urls(base_url, provider_manager)) # type: ignore
114101

115102
web_app.add_handlers(host_pattern, handlers)
103+
104+
# Log enterprise mode status and LiteLLM configuration
105+
if is_enterprise():
106+
server_app.log.info("Enterprise mode enabled")
107+
if is_litellm_configured():
108+
server_app.log.info(f"LiteLLM configured: endpoint={constants.LITELLM_BASE_URL}, models={constants.LITELLM_MODELS}")
109+
116110
server_app.log.info("Loaded the mito_ai server extension")

mito-ai/mito_ai/anthropic_client.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem, MessageType
1010
from mito_ai.completions.prompt_builders.prompt_section_registry import get_max_trim_after_messages
1111
from openai.types.chat import ChatCompletionMessageParam
12-
from mito_ai.utils.anthropic_utils import get_anthropic_completion_from_mito_server, select_correct_model, stream_anthropic_completion_from_mito_server, get_anthropic_completion_function_params
12+
from mito_ai.utils.anthropic_utils import get_anthropic_completion_from_mito_server, select_correct_model, stream_anthropic_completion_from_mito_server, get_anthropic_completion_function_params, LARGE_CONTEXT_MODEL, EXTENDED_CONTEXT_BETA
1313

1414
# Max tokens is a required parameter for the Anthropic API.
1515
# We set it to a high number so that we can edit large code cells
@@ -220,7 +220,10 @@ def __init__(self, api_key: Optional[str], timeout: int = 30, max_retries: int =
220220
self.max_retries = max_retries
221221
self.client: Optional[anthropic.Anthropic]
222222
if api_key:
223-
self.client = anthropic.Anthropic(api_key=api_key)
223+
# Use a higher timeout to avoid the 10-minute streaming requirement for long requests
224+
# The default SDK timeout is 600s (10 minutes), but we set it higher for agent mode
225+
# TODO: We should update agent mode to use streaming like anthropic suggests
226+
self.client = anthropic.Anthropic(api_key=api_key, timeout=1200.0) # 20 minutes
224227
else:
225228
self.client = None
226229

@@ -249,7 +252,8 @@ async def request_completions(
249252
if self.api_key:
250253
# Unpack provider_data for direct API call
251254
assert self.client is not None
252-
response = self.client.messages.create(**provider_data)
255+
# Beta API accepts MessageParam (compatible at runtime with BetaMessageParam)
256+
response = self.client.beta.messages.create(**provider_data) # type: ignore[arg-type]
253257

254258
if provider_data.get("tool_choice") is not None:
255259
result = extract_and_parse_anthropic_json_response(response)
@@ -284,21 +288,27 @@ async def stream_completions(self, messages: List[ChatCompletionMessageParam], m
284288

285289
if self.api_key:
286290
assert self.client is not None
287-
stream = self.client.messages.create(
288-
model=model,
289-
max_tokens=MAX_TOKENS,
290-
temperature=0,
291-
system=anthropic_system_prompt,
292-
messages=anthropic_messages,
293-
stream=True
294-
)
291+
# Beta API accepts MessageParam (compatible at runtime with BetaMessageParam)
292+
# Enable extended context beta when using LARGE_CONTEXT_MODEL
293+
create_params = {
294+
"model": model,
295+
"max_tokens": MAX_TOKENS,
296+
"temperature": 0,
297+
"system": anthropic_system_prompt,
298+
"messages": anthropic_messages, # type: ignore[arg-type]
299+
"stream": True
300+
}
301+
if model == LARGE_CONTEXT_MODEL:
302+
create_params["betas"] = [EXTENDED_CONTEXT_BETA]
303+
stream = self.client.beta.messages.create(**create_params) # type: ignore[call-overload]
295304

296305
for chunk in stream:
297-
if chunk.type == "content_block_delta" and chunk.delta.type == "text_delta":
298-
content = chunk.delta.text
306+
# Type checking for beta API streaming chunks (runtime type checking, types are compatible)
307+
if chunk.type == "content_block_delta" and chunk.delta.type == "text_delta": # type: ignore[union-attr]
308+
content = chunk.delta.text # type: ignore[union-attr]
299309
accumulated_response += content
300310

301-
is_finished = chunk.type == "message_stop"
311+
is_finished = chunk.type == "message_stop" # type: ignore[union-attr]
302312

303313
reply_fn(CompletionStreamChunk(
304314
parent_id=message_id,

0 commit comments

Comments
 (0)