mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-17 23:51:30 +00:00
Merge branch 'main' into newrelic
This commit is contained in:
commit
e3dac64fd1
25 changed files with 294 additions and 6270 deletions
|
|
@ -632,7 +632,9 @@ curl -X POST 'http://0.0.0.0:4000/chat/completions' \
|
|||
|
||||
## OpenAI Chat Completion to Responses API Bridge
|
||||
|
||||
Call any Responses API model from OpenAI's `/chat/completions` endpoint.
|
||||
LiteLLM offers a chat completion to Responses API bridge. This lets you use the completion interface while calling the Responses API under the hood.
|
||||
|
||||
This is useful when you want to use [Responses API](https://platform.openai.com/docs/api-reference/responses) specific features (like built-in tools, web search preview, or code interpreter).
|
||||
|
||||
:::tip gpt-5.4 + reasoning_effort + function tools
|
||||
|
||||
|
|
@ -649,12 +651,54 @@ response = litellm.completion(
|
|||
|
||||
:::
|
||||
|
||||
### When to use the `openai/responses/` prefix
|
||||
|
||||
Each model has a `mode` property defined in [`model_prices_and_context_window.json`](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json) that determines which API endpoint it uses by default:
|
||||
|
||||
- **`mode: responses`** - Model automatically uses the Responses API
|
||||
- **`mode: chat`** - Model defaults to the Chat Completions API
|
||||
|
||||
**Models with `mode: responses`** (automatic Responses API):
|
||||
- `o3-deep-research`, `o4-mini-deep-research`
|
||||
- `o1-pro`, `o3-pro`
|
||||
- `gpt-5.1-codex`, `gpt-5.1-codex-mini`, `gpt-5.1-codex-max`
|
||||
- `codex-mini-latest`
|
||||
|
||||
**Models with `mode: chat`** (require `openai/responses/` prefix for built-in tools):
|
||||
- `gpt-4o`, `gpt-4o-mini`, `gpt-4.1`, `gpt-4.1-mini`
|
||||
- `gpt-5`, `gpt-5-mini`
|
||||
- `o3`, `o4-mini`
|
||||
|
||||
To use built-in tools like `web_search_preview` with `mode: chat` models, add the `openai/responses/` prefix:
|
||||
|
||||
```python
|
||||
# This will FAIL - gpt-4o has mode: chat, uses Chat Completions API
|
||||
response = litellm.completion(
|
||||
model="gpt-4o",
|
||||
messages=[{"role": "user", "content": "What is the weather in Paris today?"}],
|
||||
tools=[{"type": "web_search_preview"}], # Not supported in Chat Completions
|
||||
# ... other kwargs
|
||||
)
|
||||
|
||||
# This will WORK - prefix forces Responses API
|
||||
response = litellm.completion(
|
||||
model="openai/responses/gpt-4o",
|
||||
messages=[{"role": "user", "content": "What is the weather in Paris today?"}],
|
||||
tools=[{"type": "web_search_preview"}], # Supported in Responses API
|
||||
# ... other kwargs
|
||||
)
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="sdk" label="SDK">
|
||||
|
||||
**Using a model with `mode: responses` (automatic):**
|
||||
|
||||
```python
|
||||
import litellm
|
||||
import os
|
||||
import os
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "sk-1234"
|
||||
|
||||
|
|
@ -668,6 +712,26 @@ response = litellm.completion(
|
|||
)
|
||||
print(response)
|
||||
```
|
||||
|
||||
**Using a model with `mode: chat` (requires prefix):**
|
||||
|
||||
```python
|
||||
import litellm
|
||||
import os
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "sk-1234"
|
||||
|
||||
# Use the openai/responses/ prefix to enable built-in tools
|
||||
response = litellm.completion(
|
||||
model="openai/responses/gpt-4o",
|
||||
messages=[{"role": "user", "content": "What is the weather in Paris today?"}],
|
||||
tools=[
|
||||
{"type": "web_search_preview"},
|
||||
],
|
||||
)
|
||||
print(response)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="proxy" label="PROXY">
|
||||
|
||||
|
|
@ -675,10 +739,17 @@ print(response)
|
|||
|
||||
```yaml
|
||||
model_list:
|
||||
- model_name: openai-model
|
||||
# Model with mode: responses (automatic)
|
||||
- model_name: o3-deep-research
|
||||
litellm_params:
|
||||
model: o3-deep-research-2025-06-26
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
# Model with mode: chat (use prefix for built-in tools)
|
||||
- model_name: gpt-4o-with-tools
|
||||
litellm_params:
|
||||
model: openai/responses/gpt-4o
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
```
|
||||
|
||||
2. Start the proxy
|
||||
|
|
@ -693,15 +764,14 @@ litellm --config config.yaml
|
|||
curl -X POST 'http://0.0.0.0:4000/chat/completions' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer sk-1234' \
|
||||
-d '{
|
||||
"model": "openai-model",
|
||||
-d '{
|
||||
"model": "gpt-4o-with-tools",
|
||||
"messages": [
|
||||
{"role": "user", "content": "What is the capital of France?"}
|
||||
{"role": "user", "content": "What is the weather in Paris today?"}
|
||||
],
|
||||
"tools": [
|
||||
{"type": "web_search_preview"},
|
||||
{"type": "code_interpreter", "container": {"type": "auto"}},
|
||||
],
|
||||
{"type": "web_search_preview"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
|
|
|
|||
143
docs/my-website/docs/tutorials/retool_assist.md
Normal file
143
docs/my-website/docs/tutorials/retool_assist.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import Image from '@theme/IdealImage';
|
||||
|
||||
# Retool Assist
|
||||
|
||||
This guide walks you through connecting [Retool Assist](https://docs.retool.com/apps/guides/assist/) to LiteLLM Proxy. Retool Assist uses AI to generate and edit apps from within the Retool app IDE. Using LiteLLM with Retool Assist allows you to:
|
||||
|
||||
- Access 100+ LLMs through Retool Assist
|
||||
- Track spend and usage, set budget limits per virtual key
|
||||
- Control which models Retool Assist can access
|
||||
- Use your own LLM providers via a unified OpenAI-compatible API
|
||||
|
||||
<div style={{ maxWidth: '100%', overflow: 'hidden', paddingBottom: '59.52%', position: 'relative', height: 0 }}>
|
||||
<iframe
|
||||
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', maxWidth: '840px' }}
|
||||
src="https://www.youtube.com/embed/aN-Iua5dHGg"
|
||||
frameborder="0"
|
||||
webkitallowfullscreen
|
||||
mozallowfullscreen
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
:::info
|
||||
**Hosted Retool requires a public URL.** Retool Cloud runs on Retool's servers, so `localhost` will not work. You must expose your LiteLLM proxy via ngrok, Cloudflare Tunnel, or by deploying to a cloud provider.
|
||||
:::
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Provider Schema | OpenAI |
|
||||
| Base URL | Your ngrok URL (e.g. `https://abc123.ngrok-free.app`) or deployed proxy URL |
|
||||
| API Key | Your LiteLLM Virtual Key |
|
||||
| Model | Public model name from LiteLLM (e.g. `openai/gpt-4o-mini`, `openai/gpt-5.2-2025-12-11`) |
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- LiteLLM Proxy running locally or deployed
|
||||
- [ngrok](https://ngrok.com/download) (or similar tunnel) for local development with hosted Retool
|
||||
- A [Retool](https://retool.com) account (Cloud or self-hosted)
|
||||
|
||||
## 1. Start LiteLLM Proxy
|
||||
|
||||
Set up LiteLLM Proxy following the [Getting Started Guide](https://docs.litellm.ai/docs/proxy/docker_quick_start). Ensure your proxy is running on port 4000.
|
||||
|
||||
## 2. Expose LiteLLM with a Public URL
|
||||
|
||||
<Image img={require('../../img/ngrok_public_url.gif')} />
|
||||
|
||||
Retool Cloud runs on Retool's servers. You must expose your local LiteLLM proxy with a public URL.
|
||||
|
||||
### Using ngrok
|
||||
|
||||
- Install [ngrok](https://ngrok.com/download)
|
||||
- In a separate terminal, run:
|
||||
|
||||
```bash
|
||||
ngrok http 4000
|
||||
```
|
||||
- Copy the generated HTTPS URL (e.g. `https://abc123.ngrok-free.app`). This is your **Base URL** for Retool.
|
||||
|
||||
|
||||
### Alternative
|
||||
|
||||
If you deploy LiteLLM to Railway, Render, Fly.io, or another cloud provider, use that public URL as your Base URL. See the [Deploy guide](https://docs.litellm.ai/docs/proxy/deploy) for details.
|
||||
|
||||
## 3. Generate a Virtual Key
|
||||
|
||||
<Image img={require('../../img/litellm_virtual_key.gif')} />
|
||||
|
||||
Create a virtual key that Retool Assist will use to authenticate with LiteLLM. The key must have access to the models you want to use (e.g. `openai/*` for all OpenAI models).
|
||||
|
||||
### Via LiteLLM UI
|
||||
|
||||
- Navigate to [http://localhost:4000/ui](http://localhost:4000/ui)
|
||||
- Go to **Virtual Keys** → **+ Create New Key**
|
||||
- Select the models you need (or `openai/*` for all OpenAI models)
|
||||
- Copy the key
|
||||
|
||||
## 4. Add LiteLLM as a Custom Provider in Retool
|
||||
|
||||
Inside your Retool dashboard, configure LiteLLM as a custom AI resource:
|
||||
|
||||
<Image img={require('../../img/retool_resource_setup.gif')} />
|
||||
|
||||
1. Go to **Resources**
|
||||
|
||||
2. Under the **AI** category, select **Custom Provider**
|
||||
|
||||
3. Fill in the form:
|
||||
- **Name:** `LiteLLM`
|
||||
- **Description:** (optional) e.g. `LiteLLM Proxy - 100+ LLMs`
|
||||
- **Provider Schema:** `OpenAI`
|
||||
- **Base URL:** Your ngrok-generated URL (e.g. `https://abc123.ngrok-free.app`) or deployed proxy URL—do not add `/v1` unless Retool requires it
|
||||
- **API Key:** Your LiteLLM virtual key from Step 3
|
||||
4. **Add model names** from your LiteLLM proxy (e.g. `openai/gpt-4o-mini`, `openai/gpt-5.2-2025-12-11`).
|
||||
5. Click **Create Resource**
|
||||
|
||||
<Image img={require('../../img/retool_llm_setup.gif')} />
|
||||
|
||||
## 5. Test the Connection
|
||||
|
||||
<Image img={require('../../img/retool_litellm_connection.gif')} />
|
||||
|
||||
- Open an app in Retool and enable **Assist** (if not already enabled in your organization)
|
||||
- Use Assist to generate or edit app elements, it will route requests through LiteLLM
|
||||
- Use the code option from the Sidebar to add a resource query, select the LiteLLM resource, and run it to test the setup.
|
||||
- Check the LiteLLM **Logs** section to verify requests and track usage
|
||||
|
||||
<Image img={require('../../img/retool_litellm_logs.gif')} />
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 401 Unauthorized
|
||||
|
||||
- Ensure the **API Key** in Retool matches your LiteLLM virtual key exactly
|
||||
- Verify the key is not expired or blocked in LiteLLM
|
||||
|
||||
### 401 "key not allowed to access model"
|
||||
|
||||
Your virtual key is restricted to specific models. Generate a new key with `openai/*` or include the model you need (e.g. `openai/gpt-5.2-2025-12-11`) in the key's allowed models list.
|
||||
|
||||
### 500 "api_key client option must be set"
|
||||
|
||||
LiteLLM could not use your OpenAI API key to call the provider. Ensure `OPENAI_API_KEY` is set in your LiteLLM environment (e.g. in `.env` or `docker-compose.yml`) when using `openai/*` models.
|
||||
|
||||
### localhost does not work
|
||||
|
||||
Retool Cloud cannot reach `localhost` it points to Retool's servers. Use ngrok or deploy LiteLLM to a public URL.
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Virtual Keys](https://docs.litellm.ai/docs/proxy/virtual_keys) – Create and manage API keys
|
||||
- [Deploy LiteLLM](https://docs.litellm.ai/docs/proxy/deploy) – Production deployment options
|
||||
- [Retool Assist Documentation](https://docs.retool.com/apps/guides/assist/) – Configure Assist and prompting guides
|
||||
BIN
docs/my-website/img/litellm_virtual_key.gif
Normal file
BIN
docs/my-website/img/litellm_virtual_key.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 MiB |
BIN
docs/my-website/img/ngrok_public_url.gif
Normal file
BIN
docs/my-website/img/ngrok_public_url.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 MiB |
BIN
docs/my-website/img/retool_litellm_connection.gif
Normal file
BIN
docs/my-website/img/retool_litellm_connection.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 MiB |
BIN
docs/my-website/img/retool_litellm_logs.gif
Normal file
BIN
docs/my-website/img/retool_litellm_logs.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 MiB |
BIN
docs/my-website/img/retool_llm_setup.gif
Normal file
BIN
docs/my-website/img/retool_llm_setup.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 MiB |
BIN
docs/my-website/img/retool_resource_setup.gif
Normal file
BIN
docs/my-website/img/retool_resource_setup.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 MiB |
|
|
@ -172,7 +172,8 @@ const sidebars = {
|
|||
"tutorials/litellm_gemini_cli",
|
||||
"tutorials/google_genai_sdk",
|
||||
"tutorials/litellm_qwen_code_cli",
|
||||
"tutorials/openai_codex"
|
||||
"tutorials/openai_codex",
|
||||
"tutorials/retool_assist"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1456,6 +1456,7 @@ async def test_add_update_server_with_alias():
|
|||
mock_mcp_server.authorization_url = None
|
||||
mock_mcp_server.registration_url = None
|
||||
mock_mcp_server.token_url = None
|
||||
mock_mcp_server.oauth2_flow = None
|
||||
# Additional fields used by build_mcp_server_from_table
|
||||
mock_mcp_server.extra_headers = None
|
||||
mock_mcp_server.allow_all_keys = False
|
||||
|
|
@ -1511,6 +1512,7 @@ async def test_add_update_server_without_alias():
|
|||
mock_mcp_server.authorization_url = None
|
||||
mock_mcp_server.registration_url = None
|
||||
mock_mcp_server.token_url = None
|
||||
mock_mcp_server.oauth2_flow = None
|
||||
# Additional fields used by build_mcp_server_from_table
|
||||
mock_mcp_server.extra_headers = None
|
||||
mock_mcp_server.allow_all_keys = False
|
||||
|
|
@ -1566,6 +1568,7 @@ async def test_add_update_server_fallback_to_server_id():
|
|||
mock_mcp_server.authorization_url = None
|
||||
mock_mcp_server.registration_url = None
|
||||
mock_mcp_server.token_url = None
|
||||
mock_mcp_server.oauth2_flow = None
|
||||
# Additional fields used by build_mcp_server_from_table - set explicitly
|
||||
# to avoid MagicMock objects being passed to Pydantic MCPServer constructor
|
||||
mock_mcp_server.extra_headers = None
|
||||
|
|
|
|||
|
|
@ -354,7 +354,7 @@ def test_generic_cost_per_token_anthropic_prompt_caching():
|
|||
|
||||
|
||||
def test_generic_cost_per_token_anthropic_prompt_caching_with_cache_creation():
|
||||
model = "claude-3-5-haiku-20241022"
|
||||
model = "claude-haiku-4-5-20251001"
|
||||
usage = Usage(
|
||||
completion_tokens=90,
|
||||
prompt_tokens=28436,
|
||||
|
|
@ -379,7 +379,7 @@ def test_generic_cost_per_token_anthropic_prompt_caching_with_cache_creation():
|
|||
)
|
||||
|
||||
print(f"prompt_cost: {prompt_cost}")
|
||||
assert round(prompt_cost, 3) == 0.023
|
||||
assert round(prompt_cost, 3) == 0.029
|
||||
|
||||
|
||||
def test_string_cost_values():
|
||||
|
|
|
|||
|
|
@ -219,6 +219,7 @@ class TestAgentCoreStreamingJsonFallback:
|
|||
messages=[{"role": "user", "content": "test"}],
|
||||
stream=True,
|
||||
client=client,
|
||||
api_key="test-jwt-token",
|
||||
)
|
||||
|
||||
# Collect content across all chunks
|
||||
|
|
@ -257,6 +258,7 @@ class TestAgentCoreStreamingJsonFallback:
|
|||
messages=[{"role": "user", "content": "test"}],
|
||||
stream=True,
|
||||
client=client,
|
||||
api_key="test-jwt-token",
|
||||
)
|
||||
|
||||
# Collect content across all chunks
|
||||
|
|
@ -289,6 +291,7 @@ class TestAgentCoreStreamingJsonFallback:
|
|||
messages=[{"role": "user", "content": "test"}],
|
||||
stream=True,
|
||||
client=client,
|
||||
api_key="test-jwt-token",
|
||||
)
|
||||
|
||||
async def test_async_streaming_malformed_json_raises_error(self):
|
||||
|
|
@ -316,4 +319,5 @@ class TestAgentCoreStreamingJsonFallback:
|
|||
messages=[{"role": "user", "content": "test"}],
|
||||
stream=True,
|
||||
client=client,
|
||||
api_key="test-jwt-token",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -444,9 +444,6 @@ def test_anthropic_web_search_in_model_info():
|
|||
supported_models = [
|
||||
"anthropic/claude-4-sonnet-20250514",
|
||||
"anthropic/claude-sonnet-4-5-20250929",
|
||||
"anthropic/claude-3-5-sonnet-20241022",
|
||||
"anthropic/claude-3-5-haiku-20241022",
|
||||
"anthropic/claude-3-5-haiku-latest",
|
||||
]
|
||||
for model in supported_models:
|
||||
from litellm.utils import get_model_info
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ describe("useMCPServers", () => {
|
|||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(networking.fetchMCPServers).toHaveBeenCalledWith(mockAccessToken);
|
||||
expect(networking.fetchMCPServers).toHaveBeenCalledWith(mockAccessToken, undefined);
|
||||
expect(result.current.data).toEqual(mockServers);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -20,11 +20,15 @@ vi.mock("@/components/molecules/notifications_manager", () => ({
|
|||
|
||||
// Mock react-query
|
||||
const mockInvalidateQueries = vi.fn();
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQueryClient: () => ({
|
||||
invalidateQueries: mockInvalidateQueries,
|
||||
}),
|
||||
}));
|
||||
vi.mock("@tanstack/react-query", async (importOriginal) => {
|
||||
const actual = await importOriginal() as any;
|
||||
return {
|
||||
...actual,
|
||||
useQueryClient: () => ({
|
||||
invalidateQueries: mockInvalidateQueries,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the useModelsInfo hook
|
||||
const mockUseModelsInfo = vi.fn(() => ({
|
||||
|
|
@ -553,7 +557,7 @@ describe("AllModelsTab", () => {
|
|||
|
||||
mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null, refetch: vi.fn() });
|
||||
|
||||
render(<AllModelsTab {...defaultProps} />);
|
||||
renderWithProviders(<AllModelsTab {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4-delete-test")).toBeInTheDocument();
|
||||
|
|
@ -597,7 +601,7 @@ describe("AllModelsTab", () => {
|
|||
|
||||
mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null, refetch: vi.fn() });
|
||||
|
||||
render(<AllModelsTab {...defaultProps} />);
|
||||
renderWithProviders(<AllModelsTab {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4-clickable")).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ describe("MCPToolPermissions", () => {
|
|||
|
||||
// Verify API calls
|
||||
// Note: useMCPServers uses useAuthorized() internally, which returns "123" from global mock
|
||||
expect(networking.fetchMCPServers).toHaveBeenCalledWith("123");
|
||||
expect(networking.fetchMCPServers).toHaveBeenCalledWith("123", undefined);
|
||||
// listMCPTools uses the accessToken prop directly
|
||||
expect(networking.listMCPTools).toHaveBeenCalledWith(mockAccessToken, mockServerId);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,8 +53,24 @@ const getServerNameInput = () => document.getElementById("server_name") as HTMLI
|
|||
/** Helper: select a dropdown option by opening a select near a label and clicking an option */
|
||||
async function selectAntOption(labelText: string, optionText: string) {
|
||||
const label = screen.getByText(labelText);
|
||||
const formItem = label.closest(".ant-form-item")!;
|
||||
const select = formItem.querySelector(".ant-select");
|
||||
// First try to find a .ant-form-item ancestor (standard form fields)
|
||||
let select: Element | null = null;
|
||||
const formItem = label.closest(".ant-form-item");
|
||||
if (formItem) {
|
||||
select = formItem.querySelector(".ant-select");
|
||||
}
|
||||
// If not found, try .ant-collapse-content ancestor (auth type is inside a Collapse panel)
|
||||
if (!select) {
|
||||
const collapseContent = label.closest(".ant-collapse-item");
|
||||
if (collapseContent) {
|
||||
select = collapseContent.querySelector(".ant-select");
|
||||
}
|
||||
}
|
||||
// Fallback: look for a sibling or nearby select
|
||||
if (!select) {
|
||||
const parent = label.closest("div");
|
||||
select = parent?.querySelector(".ant-select") ?? null;
|
||||
}
|
||||
act(() => {
|
||||
fireEvent.mouseDown(select!.querySelector(".ant-select-selector")!);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ describe("MCPServers", () => {
|
|||
|
||||
// Verify the API was called
|
||||
// Note: useMCPServers uses useAuthorized() internally, which returns "123" from global mock
|
||||
expect(networking.fetchMCPServers).toHaveBeenCalledWith("123");
|
||||
expect(networking.fetchMCPServers).toHaveBeenCalledWith("123", undefined);
|
||||
});
|
||||
|
||||
it("should fetch and merge health status for servers", async () => {
|
||||
|
|
|
|||
|
|
@ -612,18 +612,19 @@ describe("columns", () => {
|
|||
|
||||
it("should allow Admin to delete DB models", async () => {
|
||||
const user = userEvent.setup();
|
||||
const setSelectedModelId = vi.fn();
|
||||
const onDeleteClick = vi.fn();
|
||||
const cols = columns(
|
||||
"Admin",
|
||||
"admin-user",
|
||||
defaultProps.premiumUser,
|
||||
setSelectedModelId,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
onDeleteClick,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
|
|
@ -639,23 +640,24 @@ describe("columns", () => {
|
|||
expect(deleteButton).toBeInTheDocument();
|
||||
|
||||
await user.click(deleteButton);
|
||||
expect(setSelectedModelId).toHaveBeenCalledWith("deletable-model");
|
||||
expect(onDeleteClick).toHaveBeenCalledWith("deletable-model");
|
||||
});
|
||||
|
||||
it("should allow model creator to delete their own DB models", async () => {
|
||||
const user = userEvent.setup();
|
||||
const setSelectedModelId = vi.fn();
|
||||
const onDeleteClick = vi.fn();
|
||||
const cols = columns(
|
||||
"User",
|
||||
"model-creator",
|
||||
defaultProps.premiumUser,
|
||||
setSelectedModelId,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
onDeleteClick,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
|
|
@ -672,7 +674,7 @@ describe("columns", () => {
|
|||
expect(deleteButton).toBeInTheDocument();
|
||||
|
||||
await user.click(deleteButton);
|
||||
expect(setSelectedModelId).toHaveBeenCalledWith("user-model");
|
||||
expect(onDeleteClick).toHaveBeenCalledWith("user-model");
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ import CodeInterpreterOutput from "./CodeInterpreterOutput";
|
|||
import CodeInterpreterTool from "./CodeInterpreterTool";
|
||||
import { generateCodeSnippet } from "./CodeSnippets";
|
||||
import EndpointSelector from "./EndpointSelector";
|
||||
import MCPEventsDisplay, { MCPEvent } from "./MCPEventsDisplay";
|
||||
import MCPEventsDisplay from "./MCPEventsDisplay";
|
||||
import type { MCPEvent } from "../../mcp_tools/types";
|
||||
import { EndpointType, getEndpointType } from "./mode_endpoint_mapping";
|
||||
import ReasoningContent from "./ReasoningContent";
|
||||
import ResponseMetrics, { TokenUsage } from "./ResponseMetrics";
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import { ChatCompletionMessageParam } from "openai/resources/chat/completions";
|
|||
import { TokenUsage } from "../chat_ui/ResponseMetrics";
|
||||
import { VectorStoreSearchResponse } from "../chat_ui/types";
|
||||
import { getProxyBaseUrl } from "@/components/networking";
|
||||
import { MCPServer } from "../../mcp_tools/types";
|
||||
import { MCPEvent } from "../chat_ui/MCPEventsDisplay";
|
||||
import { MCPServer, type MCPEvent } from "../../mcp_tools/types";
|
||||
|
||||
export async function makeOpenAIChatCompletionRequest(
|
||||
chatHistory: { role: string; content: string | any[] }[],
|
||||
|
|
|
|||
|
|
@ -127,15 +127,15 @@ describe("responses_api", () => {
|
|||
expect(callArgs.tools).toEqual([
|
||||
{
|
||||
type: "mcp",
|
||||
server_label: "litellm",
|
||||
server_url: "litellm_proxy/mcp/alpha",
|
||||
server_label: "Alpha",
|
||||
server_url: "https://example.com/mcp/Alpha",
|
||||
require_approval: "never",
|
||||
allowed_tools: ["toolA"],
|
||||
},
|
||||
{
|
||||
type: "mcp",
|
||||
server_label: "litellm",
|
||||
server_url: "litellm_proxy/mcp/Beta",
|
||||
server_label: "Beta",
|
||||
server_url: "https://example.com/mcp/Beta",
|
||||
require_approval: "never",
|
||||
allowed_tools: ["toolB", "toolC"],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
setLoading(true);
|
||||
const _modelHubData = await modelHubPublicModelsCall();
|
||||
console.log("ModelHubData:", _modelHubData);
|
||||
setModelHubData(_modelHubData);
|
||||
setModelHubData(Array.isArray(_modelHubData) ? _modelHubData : []);
|
||||
} catch (error) {
|
||||
console.error("There was an error fetching the public model data", error);
|
||||
setServiceStatus("Service unavailable");
|
||||
|
|
@ -150,7 +150,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
setAgentLoading(true);
|
||||
const _agentHubData = await agentHubPublicModelsCall();
|
||||
console.log("AgentHubData:", _agentHubData);
|
||||
setAgentHubData(_agentHubData);
|
||||
setAgentHubData(Array.isArray(_agentHubData) ? _agentHubData : []);
|
||||
} catch (error) {
|
||||
console.error("There was an error fetching the public agent data", error);
|
||||
} finally {
|
||||
|
|
@ -163,7 +163,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
setMcpLoading(true);
|
||||
const _mcpHubData = await mcpHubPublicServersCall();
|
||||
console.log("MCPHubData:", _mcpHubData);
|
||||
setMcpHubData(_mcpHubData);
|
||||
setMcpHubData(Array.isArray(_mcpHubData) ? _mcpHubData : []);
|
||||
} catch (error) {
|
||||
console.error("There was an error fetching the public MCP server data", error);
|
||||
} finally {
|
||||
|
|
@ -199,7 +199,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
const getUniqueProviders = (data: ModelGroupInfo[]) => {
|
||||
const providers = new Set<string>();
|
||||
data.forEach((model) => {
|
||||
model.providers.forEach((provider) => providers.add(provider));
|
||||
(model.providers ?? []).forEach((provider) => providers.add(provider));
|
||||
});
|
||||
return Array.from(providers);
|
||||
};
|
||||
|
|
@ -532,7 +532,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
accessorKey: "providers",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const providers = row.original.providers;
|
||||
const providers = row.original.providers ?? [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
|
|
@ -760,7 +760,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
accessorKey: "description",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const description = row.original.description;
|
||||
const description = row.original.description ?? "";
|
||||
const truncated = description.length > 80 ? description.substring(0, 80) + "..." : description;
|
||||
return (
|
||||
<Tooltip title={description}>
|
||||
|
|
@ -897,7 +897,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
accessorKey: "mcp_info.description",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const description = row.original.mcp_info?.description || "-";
|
||||
const description = String(row.original.mcp_info?.description ?? "-");
|
||||
const truncated = description.length > 80 ? description.substring(0, 80) + "..." : description;
|
||||
return (
|
||||
<Tooltip title={description}>
|
||||
|
|
@ -912,7 +912,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
accessorKey: "url",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const url = row.original.url;
|
||||
const url = row.original.url ?? "";
|
||||
const truncated = url.length > 40 ? url.substring(0, 40) + "..." : url;
|
||||
return (
|
||||
<Tooltip title={url}>
|
||||
|
|
@ -1336,7 +1336,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
<div>
|
||||
<Text className="font-medium">Providers:</Text>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{selectedModel.providers.map((provider) => {
|
||||
{(selectedModel.providers ?? []).map((provider) => {
|
||||
const { logo } = getProviderLogoAndName(provider);
|
||||
return (
|
||||
<Tag key={provider} color="blue">
|
||||
|
|
@ -1460,7 +1460,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
)}
|
||||
|
||||
{/* Supported OpenAI Parameters */}
|
||||
{selectedModel.supported_openai_params && (
|
||||
{selectedModel.supported_openai_params && selectedModel.supported_openai_params.length > 0 && (
|
||||
<div>
|
||||
<Text className="text-lg font-semibold mb-4">Supported OpenAI Parameters</Text>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
|
@ -1634,7 +1634,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
<div>
|
||||
<Text className="font-medium">Input Modes:</Text>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{selectedAgent.defaultInputModes?.map((mode) => (
|
||||
{(selectedAgent.defaultInputModes ?? []).map((mode) => (
|
||||
<Tag key={mode} color="blue">
|
||||
{mode}
|
||||
</Tag>
|
||||
|
|
@ -1644,7 +1644,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
<div>
|
||||
<Text className="font-medium">Output Modes:</Text>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{selectedAgent.defaultOutputModes?.map((mode) => (
|
||||
{(selectedAgent.defaultOutputModes ?? []).map((mode) => (
|
||||
<Tag key={mode} color="blue">
|
||||
{mode}
|
||||
</Tag>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue