Merge branch 'main' into newrelic

This commit is contained in:
Josh Bonczkowski 2026-03-11 16:39:25 -04:00
commit e3dac64fd1
25 changed files with 294 additions and 6270 deletions

View file

@ -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"}
]
}'
```

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 MiB

View file

@ -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

View file

@ -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

View file

@ -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():

View file

@ -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",
)

View file

@ -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

View file

@ -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);
});

View file

@ -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();

View file

@ -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);
});

View file

@ -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")!);
});

View file

@ -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 () => {

View file

@ -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");
});

View file

@ -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";

View file

@ -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[] }[],

View file

@ -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"],
},

View file

@ -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>