From 950b7cef44f14b2db1429f6fbd32548a7c95d325 Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Mon, 29 Sep 2025 17:31:39 -0400 Subject: [PATCH 1/6] added oauth mcp to docs --- docs/my-website/docs/mcp.md | 407 ++++++++++++------------------------ 1 file changed, 133 insertions(+), 274 deletions(-) diff --git a/docs/my-website/docs/mcp.md b/docs/my-website/docs/mcp.md index 7eee979cc67..f99d731d3cf 100644 --- a/docs/my-website/docs/mcp.md +++ b/docs/my-website/docs/mcp.md @@ -137,7 +137,6 @@ mcp_servers: | `basic` | `Authorization: Basic ` | | `authorization` | `Authorization: ` | -- **Extra Headers**: Optional list of additional header names that should be forwarded from client to the MCP server - **Spec Version**: Optional MCP specification version (defaults to `2025-06-18`) Examples for each auth type: @@ -149,16 +148,6 @@ mcp_servers: auth_type: "api_key" auth_value: "abc123" # headers={"X-API-Key": "abc123"} - # NEW – OAuth 2.0 Client Credentials (v1.77.5) - oauth2_example: - url: "https://my-mcp-server.com/mcp" - auth_type: "oauth2" # 👈 KEY CHANGE - authorization_url: "https://my-mcp-server.com/oauth/authorize" # optional for client-credentials - token_url: "https://my-mcp-server.com/oauth/token" # required - client_id: os.environ/OAUTH_CLIENT_ID - client_secret: os.environ/OAUTH_CLIENT_SECRET - scopes: ["tool.read", "tool.write"] # optional - bearer_example: url: "https://my-mcp-server.com/mcp" auth_type: "bearer_token" @@ -173,13 +162,6 @@ mcp_servers: url: "https://my-mcp-server.com/mcp" auth_type: "authorization" auth_value: "Token example123" # headers={"Authorization": "Token example123"} - - # Example with extra headers forwarding - github_mcp: - url: "https://api.githubcopilot.com/mcp" - auth_type: "bearer_token" - auth_value: "ghp_example_token" - extra_headers: ["custom_key", "x-custom-header"] # These headers will be forwarded from client ``` @@ -209,65 +191,6 @@ litellm_settings: -## MCP Tool Filtering - -Control which tools are available from your MCP servers. You can either allow only specific tools or block dangerous ones. - - - - -Use `allowed_tools` to specify exactly which tools users can access. All other tools will be blocked. - -```yaml title="config.yaml" showLineNumbers -mcp_servers: - github_mcp: - url: "https://api.githubcopilot.com/mcp" - auth_type: oauth2 - authorization_url: https://github.com/login/oauth/authorize - token_url: https://github.com/login/oauth/access_token - client_id: os.environ/GITHUB_OAUTH_CLIENT_ID - client_secret: os.environ/GITHUB_OAUTH_CLIENT_SECRET - scopes: ["public_repo", "user:email"] - allowed_tools: ["list_tools"] - # only list_tools will be available -``` - -**Use this when:** -- You want strict control over which tools are available -- You're in a high-security environment -- You're testing a new MCP server with limited tools - - - - -Use `disallowed_tools` to block specific tools. All other tools will be available. - -```yaml title="config.yaml" showLineNumbers -mcp_servers: - github_mcp: - url: "https://api.githubcopilot.com/mcp" - auth_type: oauth2 - authorization_url: https://github.com/login/oauth/authorize - token_url: https://github.com/login/oauth/access_token - client_id: os.environ/GITHUB_OAUTH_CLIENT_ID - client_secret: os.environ/GITHUB_OAUTH_CLIENT_SECRET - scopes: ["public_repo", "user:email"] - disallowed_tools: ["repo_delete"] - # only repo_delete will be blocked -``` - -**Use this when:** -- Most tools are safe, but you want to block a few dangerous ones -- You want to prevent expensive API calls -- You're gradually adding restrictions to an existing server - - - - -### Important Notes - -- If you specify both `allowed_tools` and `disallowed_tools`, the allowed list takes priority -- Tool names are case-sensitive ## Using your MCP @@ -848,203 +771,6 @@ When creating API keys, you can assign them to specific access groups for permis /> -## Forwarding Custom Headers to MCP Servers - -LiteLLM supports forwarding additional custom headers from MCP clients to backend MCP servers using the `extra_headers` configuration parameter. This allows you to pass custom authentication tokens, API keys, or other headers that your MCP server requires. - -### Configuration - - - - -Configure `extra_headers` in your MCP server configuration to specify which header names should be forwarded: - -```yaml title="config.yaml with extra_headers" showLineNumbers -mcp_servers: - github_mcp: - url: "https://api.githubcopilot.com/mcp" - auth_type: "bearer_token" - auth_value: "ghp_default_token" - extra_headers: ["custom_key", "x-custom-header", "Authorization"] - description: "GitHub MCP server with custom header forwarding" -``` - - - -Use this when giving users access to a [group of MCP servers](#grouping-mcps-access-groups). - -**Format:** `x-mcp-{server_alias}-{header_name}: value` - -This allows you to use different authentication for different MCP servers. - - -**Examples:** -- `x-mcp-github-authorization: Bearer ghp_xxxxxxxxx` - GitHub MCP server with Bearer token -- `x-mcp-zapier-x-api-key: sk-xxxxxxxxx` - Zapier MCP server with API key -- `x-mcp-deepwiki-authorization: Basic base64_encoded_creds` - DeepWiki MCP server with Basic auth - -```python title="Python Client with Server-Specific Auth" showLineNumbers -from fastmcp import Client -import asyncio - -# Standard MCP configuration with multiple servers -config = { - "mcpServers": { - "mcp_group": { - "url": "http://localhost:4000/mcp", - "headers": { - "x-mcp-servers": "dev_group", # assume this gives access to github, zapier and deepwiki - "x-litellm-api-key": "Bearer sk-1234", - "x-mcp-github-authorization": "Bearer gho_token", - "x-mcp-zapier-x-api-key": "sk-xxxxxxxxx", - "x-mcp-deepwiki-authorization": "Basic base64_encoded_creds", - "custom_key": "value" - } - } - } -} - -# Create a client that connects to all servers -client = Client(config) - - -async def main(): - async with client: - tools = await client.list_tools() - print(f"Available tools: {tools}") - - # call mcp - await client.call_tool( - name="github_mcp-search_issues", - arguments={'query': 'created:>2024-01-01', 'sort': 'created', 'order': 'desc', 'perPage': 30} - ) - -if __name__ == "__main__": - asyncio.run(main()) - -``` - - - -**Benefits:** -- **Server-specific authentication**: Each MCP server can use different auth methods -- **Better security**: No need to share the same auth token across all servers -- **Flexible header names**: Support for different auth header types (authorization, x-api-key, etc.) -- **Clean separation**: Each server's auth is clearly identified - - - - - - - -### Client Usage - -When connecting from MCP clients, include the custom headers that match the `extra_headers` configuration: - - - - -```python title="FastMCP Client with Custom Headers" showLineNumbers -from fastmcp import Client -import asyncio - -# MCP client configuration with custom headers -config = { - "mcpServers": { - "github": { - "url": "http://localhost:4000/github_mcp/mcp", - "headers": { - "x-litellm-api-key": "Bearer sk-1234", - "Authorization": "Bearer gho_token", - "custom_key": "custom_value", - "x-custom-header": "additional_data" - } - } - } -} - -# Create a client that connects to the server -client = Client(config) - -async def main(): - async with client: - # List available tools - tools = await client.list_tools() - print(f"Available tools: {tools}") - - # Call a tool if available - if tools: - result = await client.call_tool(tools[0].name, {}) - print(f"Tool result: {result}") - -# Run the client -asyncio.run(main()) -``` - - - - - -```json title="Cursor MCP Configuration with Custom Headers" showLineNumbers -{ - "mcpServers": { - "GitHub": { - "url": "http://localhost:4000/github_mcp/mcp", - "headers": { - "x-litellm-api-key": "Bearer $LITELLM_API_KEY", - "Authorization": "Bearer $GITHUB_TOKEN", - "custom_key": "custom_value", - "x-custom-header": "additional_data" - } - } - } -} -``` - - - - - -```bash title="cURL with Custom Headers" showLineNumbers -curl --location 'http://localhost:4000/github_mcp/mcp' \ ---header 'Content-Type: application/json' \ ---header 'x-litellm-api-key: Bearer sk-1234' \ ---header 'Authorization: Bearer gho_token' \ ---header 'custom_key: custom_value' \ ---header 'x-custom-header: additional_data' \ ---data '{ - "jsonrpc": "2.0", - "id": 1, - "method": "tools/list" -}' -``` - - - - -### How It Works - -1. **Configuration**: Define `extra_headers` in your MCP server config with the header names you want to forward -2. **Client Headers**: Include the corresponding headers in your MCP client requests -3. **Header Forwarding**: LiteLLM automatically forwards matching headers to the backend MCP server -4. **Authentication**: The backend MCP server receives both the configured auth headers and the custom headers - -### Use Cases - -- **Custom Authentication**: Forward custom API keys or tokens required by specific MCP servers -- **Request Context**: Pass user identification, session data, or request tracking headers -- **Third-party Integration**: Include headers required by external services that your MCP server integrates with -- **Multi-tenant Systems**: Forward tenant-specific headers for proper request routing - -### Security Considerations - -- Only headers listed in `extra_headers` are forwarded to maintain security -- Sensitive headers should be passed through environment variables when possible -- Consider using server-specific auth headers for better security isolation - ---- - ## Using your MCP with client side credentials Use this if you want to pass a client side authentication token to LiteLLM to then pass to your MCP to auth to your MCP. @@ -1054,6 +780,13 @@ Use this if you want to pass a client side authentication token to LiteLLM to th You can specify MCP auth tokens using server-specific headers in the format `x-mcp-{server_alias}-{header_name}`. This allows you to use different authentication for different MCP servers. +**Format:** `x-mcp-{server_alias}-{header_name}: value` + +**Examples:** +- `x-mcp-github-authorization: Bearer ghp_xxxxxxxxx` - GitHub MCP server with Bearer token +- `x-mcp-zapier-x-api-key: sk-xxxxxxxxx` - Zapier MCP server with API key +- `x-mcp-deepwiki-authorization: Basic base64_encoded_creds` - DeepWiki MCP server with Basic auth + **Benefits:** - **Server-specific authentication**: Each MCP server can use different auth methods - **Better security**: No need to share the same auth token across all servers @@ -1433,6 +1166,132 @@ curl --location '/v1/responses' \ }' ``` +## OAuth2 Integration with MCP + +Use liteLLM to connect MCP servers using OAuth2 (Github, Zapier etc.) + +### Quick Start + + +```yaml title="config.yaml - OAuth2 MCP Configuration" showLineNumbers +model_list: + - model_name: gpt-4o + litellm_params: + model: openai/gpt-4o + api_key: sk-xxxxxxx + +mcp_servers: + github_mcp: + url: "https://api.githubcopilot.com/mcp" + auth_type: "oauth2" + client_id: "your_github_client_id" + client_secret: "your_github_client_secret" + scopes: ["public_repo", "user:email", "read:org"] + authorization_url: "https://github.com/login/oauth/authorize" + token_url: "https://github.com/login/oauth/access_token" + + zapier_mcp: + url: "https://actions.zapier.com/mcp/your-key/sse" + auth_type: "oauth2" + client_id: "zapier_client_id" + client_secret: "zapier_client_secret" + authorization_url: "https://zapier.com/oauth/authorize" + token_url: "https://zapier.com/oauth/access_token" + scopes: ["read", "write"] + + # custom mcp + custom_mcp: + url: "https://custom-mcp-url.com/mcp" + auth_type: "oauth2" + client_id: "custom_client_id" + client_secret: "custom_client_secret" + authorization_url: "https://custom-service.com/oauth/authorize" + token_url: "https://custom-service.com/oauth/token" + scopes: ["api:read", "api:write"] + redirect_uri: "https://your-app.com/callback" +``` + +### OAuth2 Endpoints + +LiteLLM automatically provides OAuth2 server discovery endpoints: + +| Endpoint | Description | +|----------|-------------| +| `/.well-known/oauth-authorization-server` | OAuth2 server metadata | +| `/authorize` | Authorization endpoint for OAuth2 flow | +| `/token` | Token exchange endpoint | +| `/callback` | OAuth2 callback handler | + +```bash title="Access OAuth2 Server Metadata" showLineNumbers +curl https://your-litellm-proxy/.well-known/oauth-authorization-server +``` + +### Using OAuth2 Tokens with MCP + +Use server-specific headers for multiple OAuth2 providers: + +```bash title="Multiple OAuth2 Servers" showLineNumbers +curl --location 'https://your-litellm-proxy/v1/responses' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer oauth_access_token' \ # or use litellm_master_key here +--data '{ + "model": "gpt-4o", + "tools": [ + { + "type": "mcp", + "server_label": "litellm", + "server_url": "litellm_proxy", + "require_approval": "never", + "headers": { + "x-litellm-api-key": "Bearer YOUR_LITELLM_API_KEY", + "x-mcp-github-authorization": "Bearer github_oauth_token_123", + "x-mcp-zapier-authorization": "Bearer zapier_oauth_token_456" + } + } + ], + "input": "Create a GitHub issue and send Zapier notification", + "tool_choice": "required" +}' +``` + +```bash title="Access all configured MCP servers" showLineNumbers +curl --location 'https://your-litellm-proxy/v1/responses' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer oauth-token' \ # or litellm_master_key +--data '{ + "model": "gpt-4o", + "tools": [ + { + "type": "mcp", + "server_label": "litellm", + "server_url": "litellm_proxy", + "require_approval": "never" + } + ], + "input": "List GitHub repositories", + "tool_choice": "required" +}' +``` + +#### Dynamic Token Refresh + +LiteLLM can automatically handle token refresh for supported providers: + +```yaml title="OAuth2 with Token Refresh" showLineNumbers +mcp_servers: + github_mcp: + url: "https://api.githubcopilot.com/mcp" + auth_type: "oauth2" + client_id: "your_client_id" + client_secret: "your_client_secret" + scopes: ["public_repo", "user:email"] + authorization_url: "https://github.com/login/oauth/authorize" + token_url: "https://github.com/login/oauth/access_token" + refresh_token_url: "https://github.com/login/oauth/access_token" # For refresh + auto_refresh: true # Enable automatic token refresh +``` + + ## MCP Cost Tracking From 93abafc9edf6a9dc4a763ea66959d91e4874b8a5 Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Thu, 2 Oct 2025 15:11:54 -0400 Subject: [PATCH 2/6] added azure ai/grok-4 model family --- model_prices_and_context_window.json | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 8fc98e5d506..4e74e58ddd4 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -3308,6 +3308,64 @@ "supports_tool_choice": true, "supports_web_search": true }, + "azure_ai/grok-4": { + "input_cost_per_token": 5.5e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.75e-05, + "source": "https://azure.microsoft.com/en-us/blog/grok-4-is-now-available-in-azure-ai-foundry-unlock-frontier-intelligence-and-business-ready-capabilities/", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "azure_ai/grok-4-fast-non-reasoning": { + "input_cost_per_token": 5e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.5e-03, + "source": "https://azure.microsoft.com/en-us/blog/grok-4-is-now-available-in-azure-ai-foundry-unlock-frontier-intelligence-and-business-ready-capabilities/", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "azure_ai/grok-4-fast-reasoning": { + "input_cost_per_token": 5.8e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.9e-03, + "source": "https://azure.microsoft.com/en-us/blog/grok-4-is-now-available-in-azure-ai-foundry-unlock-frontier-intelligence-and-business-ready-capabilities/", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "azure_ai/grok-code-fast-1": { + "input_cost_per_token": 3.5e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.75e-05, + "source": "https://azure.microsoft.com/en-us/blog/grok-4-is-now-available-in-azure-ai-foundry-unlock-frontier-intelligence-and-business-ready-capabilities/", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_web_search": true + }, "azure_ai/jais-30b-chat": { "input_cost_per_token": 0.0032, "litellm_provider": "azure_ai", From 72000be905bdfe90dcbfa5c2f8cebdb7f9d9bbf6 Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Thu, 2 Oct 2025 15:13:33 -0400 Subject: [PATCH 3/6] Revert "added oauth mcp to docs" This reverts commit 950b7cef44f14b2db1429f6fbd32548a7c95d325. --- docs/my-website/docs/mcp.md | 407 ++++++++++++++++++++++++------------ 1 file changed, 274 insertions(+), 133 deletions(-) diff --git a/docs/my-website/docs/mcp.md b/docs/my-website/docs/mcp.md index f99d731d3cf..7eee979cc67 100644 --- a/docs/my-website/docs/mcp.md +++ b/docs/my-website/docs/mcp.md @@ -137,6 +137,7 @@ mcp_servers: | `basic` | `Authorization: Basic ` | | `authorization` | `Authorization: ` | +- **Extra Headers**: Optional list of additional header names that should be forwarded from client to the MCP server - **Spec Version**: Optional MCP specification version (defaults to `2025-06-18`) Examples for each auth type: @@ -148,6 +149,16 @@ mcp_servers: auth_type: "api_key" auth_value: "abc123" # headers={"X-API-Key": "abc123"} + # NEW – OAuth 2.0 Client Credentials (v1.77.5) + oauth2_example: + url: "https://my-mcp-server.com/mcp" + auth_type: "oauth2" # 👈 KEY CHANGE + authorization_url: "https://my-mcp-server.com/oauth/authorize" # optional for client-credentials + token_url: "https://my-mcp-server.com/oauth/token" # required + client_id: os.environ/OAUTH_CLIENT_ID + client_secret: os.environ/OAUTH_CLIENT_SECRET + scopes: ["tool.read", "tool.write"] # optional + bearer_example: url: "https://my-mcp-server.com/mcp" auth_type: "bearer_token" @@ -162,6 +173,13 @@ mcp_servers: url: "https://my-mcp-server.com/mcp" auth_type: "authorization" auth_value: "Token example123" # headers={"Authorization": "Token example123"} + + # Example with extra headers forwarding + github_mcp: + url: "https://api.githubcopilot.com/mcp" + auth_type: "bearer_token" + auth_value: "ghp_example_token" + extra_headers: ["custom_key", "x-custom-header"] # These headers will be forwarded from client ``` @@ -191,6 +209,65 @@ litellm_settings: +## MCP Tool Filtering + +Control which tools are available from your MCP servers. You can either allow only specific tools or block dangerous ones. + + + + +Use `allowed_tools` to specify exactly which tools users can access. All other tools will be blocked. + +```yaml title="config.yaml" showLineNumbers +mcp_servers: + github_mcp: + url: "https://api.githubcopilot.com/mcp" + auth_type: oauth2 + authorization_url: https://github.com/login/oauth/authorize + token_url: https://github.com/login/oauth/access_token + client_id: os.environ/GITHUB_OAUTH_CLIENT_ID + client_secret: os.environ/GITHUB_OAUTH_CLIENT_SECRET + scopes: ["public_repo", "user:email"] + allowed_tools: ["list_tools"] + # only list_tools will be available +``` + +**Use this when:** +- You want strict control over which tools are available +- You're in a high-security environment +- You're testing a new MCP server with limited tools + + + + +Use `disallowed_tools` to block specific tools. All other tools will be available. + +```yaml title="config.yaml" showLineNumbers +mcp_servers: + github_mcp: + url: "https://api.githubcopilot.com/mcp" + auth_type: oauth2 + authorization_url: https://github.com/login/oauth/authorize + token_url: https://github.com/login/oauth/access_token + client_id: os.environ/GITHUB_OAUTH_CLIENT_ID + client_secret: os.environ/GITHUB_OAUTH_CLIENT_SECRET + scopes: ["public_repo", "user:email"] + disallowed_tools: ["repo_delete"] + # only repo_delete will be blocked +``` + +**Use this when:** +- Most tools are safe, but you want to block a few dangerous ones +- You want to prevent expensive API calls +- You're gradually adding restrictions to an existing server + + + + +### Important Notes + +- If you specify both `allowed_tools` and `disallowed_tools`, the allowed list takes priority +- Tool names are case-sensitive ## Using your MCP @@ -771,6 +848,203 @@ When creating API keys, you can assign them to specific access groups for permis /> +## Forwarding Custom Headers to MCP Servers + +LiteLLM supports forwarding additional custom headers from MCP clients to backend MCP servers using the `extra_headers` configuration parameter. This allows you to pass custom authentication tokens, API keys, or other headers that your MCP server requires. + +### Configuration + + + + +Configure `extra_headers` in your MCP server configuration to specify which header names should be forwarded: + +```yaml title="config.yaml with extra_headers" showLineNumbers +mcp_servers: + github_mcp: + url: "https://api.githubcopilot.com/mcp" + auth_type: "bearer_token" + auth_value: "ghp_default_token" + extra_headers: ["custom_key", "x-custom-header", "Authorization"] + description: "GitHub MCP server with custom header forwarding" +``` + + + +Use this when giving users access to a [group of MCP servers](#grouping-mcps-access-groups). + +**Format:** `x-mcp-{server_alias}-{header_name}: value` + +This allows you to use different authentication for different MCP servers. + + +**Examples:** +- `x-mcp-github-authorization: Bearer ghp_xxxxxxxxx` - GitHub MCP server with Bearer token +- `x-mcp-zapier-x-api-key: sk-xxxxxxxxx` - Zapier MCP server with API key +- `x-mcp-deepwiki-authorization: Basic base64_encoded_creds` - DeepWiki MCP server with Basic auth + +```python title="Python Client with Server-Specific Auth" showLineNumbers +from fastmcp import Client +import asyncio + +# Standard MCP configuration with multiple servers +config = { + "mcpServers": { + "mcp_group": { + "url": "http://localhost:4000/mcp", + "headers": { + "x-mcp-servers": "dev_group", # assume this gives access to github, zapier and deepwiki + "x-litellm-api-key": "Bearer sk-1234", + "x-mcp-github-authorization": "Bearer gho_token", + "x-mcp-zapier-x-api-key": "sk-xxxxxxxxx", + "x-mcp-deepwiki-authorization": "Basic base64_encoded_creds", + "custom_key": "value" + } + } + } +} + +# Create a client that connects to all servers +client = Client(config) + + +async def main(): + async with client: + tools = await client.list_tools() + print(f"Available tools: {tools}") + + # call mcp + await client.call_tool( + name="github_mcp-search_issues", + arguments={'query': 'created:>2024-01-01', 'sort': 'created', 'order': 'desc', 'perPage': 30} + ) + +if __name__ == "__main__": + asyncio.run(main()) + +``` + + + +**Benefits:** +- **Server-specific authentication**: Each MCP server can use different auth methods +- **Better security**: No need to share the same auth token across all servers +- **Flexible header names**: Support for different auth header types (authorization, x-api-key, etc.) +- **Clean separation**: Each server's auth is clearly identified + + + + + + + +### Client Usage + +When connecting from MCP clients, include the custom headers that match the `extra_headers` configuration: + + + + +```python title="FastMCP Client with Custom Headers" showLineNumbers +from fastmcp import Client +import asyncio + +# MCP client configuration with custom headers +config = { + "mcpServers": { + "github": { + "url": "http://localhost:4000/github_mcp/mcp", + "headers": { + "x-litellm-api-key": "Bearer sk-1234", + "Authorization": "Bearer gho_token", + "custom_key": "custom_value", + "x-custom-header": "additional_data" + } + } + } +} + +# Create a client that connects to the server +client = Client(config) + +async def main(): + async with client: + # List available tools + tools = await client.list_tools() + print(f"Available tools: {tools}") + + # Call a tool if available + if tools: + result = await client.call_tool(tools[0].name, {}) + print(f"Tool result: {result}") + +# Run the client +asyncio.run(main()) +``` + + + + + +```json title="Cursor MCP Configuration with Custom Headers" showLineNumbers +{ + "mcpServers": { + "GitHub": { + "url": "http://localhost:4000/github_mcp/mcp", + "headers": { + "x-litellm-api-key": "Bearer $LITELLM_API_KEY", + "Authorization": "Bearer $GITHUB_TOKEN", + "custom_key": "custom_value", + "x-custom-header": "additional_data" + } + } + } +} +``` + + + + + +```bash title="cURL with Custom Headers" showLineNumbers +curl --location 'http://localhost:4000/github_mcp/mcp' \ +--header 'Content-Type: application/json' \ +--header 'x-litellm-api-key: Bearer sk-1234' \ +--header 'Authorization: Bearer gho_token' \ +--header 'custom_key: custom_value' \ +--header 'x-custom-header: additional_data' \ +--data '{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/list" +}' +``` + + + + +### How It Works + +1. **Configuration**: Define `extra_headers` in your MCP server config with the header names you want to forward +2. **Client Headers**: Include the corresponding headers in your MCP client requests +3. **Header Forwarding**: LiteLLM automatically forwards matching headers to the backend MCP server +4. **Authentication**: The backend MCP server receives both the configured auth headers and the custom headers + +### Use Cases + +- **Custom Authentication**: Forward custom API keys or tokens required by specific MCP servers +- **Request Context**: Pass user identification, session data, or request tracking headers +- **Third-party Integration**: Include headers required by external services that your MCP server integrates with +- **Multi-tenant Systems**: Forward tenant-specific headers for proper request routing + +### Security Considerations + +- Only headers listed in `extra_headers` are forwarded to maintain security +- Sensitive headers should be passed through environment variables when possible +- Consider using server-specific auth headers for better security isolation + +--- + ## Using your MCP with client side credentials Use this if you want to pass a client side authentication token to LiteLLM to then pass to your MCP to auth to your MCP. @@ -780,13 +1054,6 @@ Use this if you want to pass a client side authentication token to LiteLLM to th You can specify MCP auth tokens using server-specific headers in the format `x-mcp-{server_alias}-{header_name}`. This allows you to use different authentication for different MCP servers. -**Format:** `x-mcp-{server_alias}-{header_name}: value` - -**Examples:** -- `x-mcp-github-authorization: Bearer ghp_xxxxxxxxx` - GitHub MCP server with Bearer token -- `x-mcp-zapier-x-api-key: sk-xxxxxxxxx` - Zapier MCP server with API key -- `x-mcp-deepwiki-authorization: Basic base64_encoded_creds` - DeepWiki MCP server with Basic auth - **Benefits:** - **Server-specific authentication**: Each MCP server can use different auth methods - **Better security**: No need to share the same auth token across all servers @@ -1166,132 +1433,6 @@ curl --location '/v1/responses' \ }' ``` -## OAuth2 Integration with MCP - -Use liteLLM to connect MCP servers using OAuth2 (Github, Zapier etc.) - -### Quick Start - - -```yaml title="config.yaml - OAuth2 MCP Configuration" showLineNumbers -model_list: - - model_name: gpt-4o - litellm_params: - model: openai/gpt-4o - api_key: sk-xxxxxxx - -mcp_servers: - github_mcp: - url: "https://api.githubcopilot.com/mcp" - auth_type: "oauth2" - client_id: "your_github_client_id" - client_secret: "your_github_client_secret" - scopes: ["public_repo", "user:email", "read:org"] - authorization_url: "https://github.com/login/oauth/authorize" - token_url: "https://github.com/login/oauth/access_token" - - zapier_mcp: - url: "https://actions.zapier.com/mcp/your-key/sse" - auth_type: "oauth2" - client_id: "zapier_client_id" - client_secret: "zapier_client_secret" - authorization_url: "https://zapier.com/oauth/authorize" - token_url: "https://zapier.com/oauth/access_token" - scopes: ["read", "write"] - - # custom mcp - custom_mcp: - url: "https://custom-mcp-url.com/mcp" - auth_type: "oauth2" - client_id: "custom_client_id" - client_secret: "custom_client_secret" - authorization_url: "https://custom-service.com/oauth/authorize" - token_url: "https://custom-service.com/oauth/token" - scopes: ["api:read", "api:write"] - redirect_uri: "https://your-app.com/callback" -``` - -### OAuth2 Endpoints - -LiteLLM automatically provides OAuth2 server discovery endpoints: - -| Endpoint | Description | -|----------|-------------| -| `/.well-known/oauth-authorization-server` | OAuth2 server metadata | -| `/authorize` | Authorization endpoint for OAuth2 flow | -| `/token` | Token exchange endpoint | -| `/callback` | OAuth2 callback handler | - -```bash title="Access OAuth2 Server Metadata" showLineNumbers -curl https://your-litellm-proxy/.well-known/oauth-authorization-server -``` - -### Using OAuth2 Tokens with MCP - -Use server-specific headers for multiple OAuth2 providers: - -```bash title="Multiple OAuth2 Servers" showLineNumbers -curl --location 'https://your-litellm-proxy/v1/responses' \ ---header 'Content-Type: application/json' \ ---header 'Authorization: Bearer oauth_access_token' \ # or use litellm_master_key here ---data '{ - "model": "gpt-4o", - "tools": [ - { - "type": "mcp", - "server_label": "litellm", - "server_url": "litellm_proxy", - "require_approval": "never", - "headers": { - "x-litellm-api-key": "Bearer YOUR_LITELLM_API_KEY", - "x-mcp-github-authorization": "Bearer github_oauth_token_123", - "x-mcp-zapier-authorization": "Bearer zapier_oauth_token_456" - } - } - ], - "input": "Create a GitHub issue and send Zapier notification", - "tool_choice": "required" -}' -``` - -```bash title="Access all configured MCP servers" showLineNumbers -curl --location 'https://your-litellm-proxy/v1/responses' \ ---header 'Content-Type: application/json' \ ---header 'Authorization: Bearer oauth-token' \ # or litellm_master_key ---data '{ - "model": "gpt-4o", - "tools": [ - { - "type": "mcp", - "server_label": "litellm", - "server_url": "litellm_proxy", - "require_approval": "never" - } - ], - "input": "List GitHub repositories", - "tool_choice": "required" -}' -``` - -#### Dynamic Token Refresh - -LiteLLM can automatically handle token refresh for supported providers: - -```yaml title="OAuth2 with Token Refresh" showLineNumbers -mcp_servers: - github_mcp: - url: "https://api.githubcopilot.com/mcp" - auth_type: "oauth2" - client_id: "your_client_id" - client_secret: "your_client_secret" - scopes: ["public_repo", "user:email"] - authorization_url: "https://github.com/login/oauth/authorize" - token_url: "https://github.com/login/oauth/access_token" - refresh_token_url: "https://github.com/login/oauth/access_token" # For refresh - auto_refresh: true # Enable automatic token refresh -``` - - ## MCP Cost Tracking From d6c877b73ac763464f204b77135f3786342373b7 Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Fri, 3 Oct 2025 23:23:04 -0400 Subject: [PATCH 4/6] fix: arize ui integration --- litellm/integrations/arize/arize.py | 88 ++++++- litellm/proxy/_types.py | 11 + .../health_endpoints/_health_endpoints.py | 15 ++ .../src/components/callback_info_helpers.tsx | 2 +- .../src/components/settings.tsx | 221 ++++++++++++++++-- 5 files changed, 315 insertions(+), 22 deletions(-) diff --git a/litellm/integrations/arize/arize.py b/litellm/integrations/arize/arize.py index 1d78e4cc69c..23822801b41 100644 --- a/litellm/integrations/arize/arize.py +++ b/litellm/integrations/arize/arize.py @@ -8,9 +8,11 @@ import os from datetime import datetime from typing import TYPE_CHECKING, Any, Optional, Union +from litellm.integrations.additional_logging_utils import AdditionalLoggingUtils from litellm.integrations.arize import _utils from litellm.integrations.opentelemetry import OpenTelemetry from litellm.types.integrations.arize import ArizeConfig +from litellm.types.integrations.base_health_check import IntegrationHealthCheckStatus from litellm.types.services import ServiceLoggerPayload from litellm.types.utils import StandardCallbackDynamicParams @@ -26,7 +28,7 @@ else: Span = Any -class ArizeLogger(OpenTelemetry): +class ArizeLogger(OpenTelemetry, AdditionalLoggingUtils): def set_attributes(self, span: Span, kwargs, response_obj: Optional[Any]): ArizeLogger.set_arize_attributes(span, kwargs, response_obj) return @@ -141,3 +143,87 @@ class ArizeLogger(OpenTelemetry): ) return dynamic_headers + + async def async_health_check(self, standard_callback_dynamic_params: Optional[StandardCallbackDynamicParams] = None) -> IntegrationHealthCheckStatus: + """ + Check if Arize service is healthy by testing OTEL trace export + + Args: + standard_callback_dynamic_params: Dynamic parameters containing arize_api_key and arize_space_key/arize_space_id + + Returns: + IntegrationHealthCheckStatus with status and optional error message + """ + try: + api_key = None + space_key = None + + if standard_callback_dynamic_params: + api_key = standard_callback_dynamic_params.get("arize_api_key") + space_key = ( + standard_callback_dynamic_params.get("arize_space_key") or + standard_callback_dynamic_params.get("arize_space_id") # fallback for backwards compatibility + ) + + if not api_key: + api_key = os.environ.get("ARIZE_API_KEY") + if not space_key: + space_key = os.environ.get("ARIZE_SPACE_KEY") + + if not api_key or not space_key: + return IntegrationHealthCheckStatus( + status="unhealthy", + error_message="Arize credentials not configured. Please set arize_api_key and arize_space_key parameters or ARIZE_API_KEY and ARIZE_SPACE_KEY environment variables." + ) + + # Get Arize configuration + arize_config = ArizeLogger.get_arize_config() + + # Validate configuration + if not arize_config.endpoint: + return IntegrationHealthCheckStatus( + status="unhealthy", + error_message="Arize endpoint not configured. Using default endpoint https://otlp.arize.com/v1" + ) + + try: + test_headers = { + "arize-space-id": space_key.strip(), + "api_key": api_key.strip(), + } + + endpoint = arize_config.endpoint or "https://otlp.arize.com/v1" + + # For a basic health check, we just validate that the configuration is properly formed + # A full test would require actually sending a trace, which might be overkill for health checks + + return IntegrationHealthCheckStatus( + status="healthy", + error_message=None + ) + + except Exception as config_error: + return IntegrationHealthCheckStatus( + status="unhealthy", + error_message=f"Arize configuration error: {str(config_error)}" + ) + + except Exception as e: + return IntegrationHealthCheckStatus( + status="unhealthy", + error_message=f"Arize health check failed: {str(e)}" + ) + + async def get_request_response_payload( + self, + request_id: str, + start_time_utc: Optional[datetime] = None, + end_time_utc: Optional[datetime] = None, + ) -> Optional[dict]: + """ + Get the request and response payload for a given request_id from Arize. + + Note: Arize is primarily for observability/tracing, not request/response storage. + This method returns None as Arize doesn't typically store raw payloads. + """ + return None diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index c5370eb7d70..292e69b6a8c 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2368,6 +2368,17 @@ class AllCallbacks(LiteLLMPydanticObjectBase): ui_callback_name="Lago Billing", ) + arize: CallbackOnUI = CallbackOnUI( + litellm_callback_name="arize", + litellm_callback_params=[ + "ARIZE_API_KEY", + "ARIZE_SPACE_KEY", + "ARIZE_ENDPOINT", + "ARIZE_HTTP_ENDPOINT", + ], + ui_callback_name="Arize", + ) + class SpendLogsMetadata(TypedDict): """ diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 883bff3185f..8b06b46f76e 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -75,6 +75,7 @@ async def health_services_endpoint( # noqa: PLR0915 "braintrust", "datadog", "generic_api", + "arize", ], str, ] = fastapi.Query(description="Specify the service being hit."), @@ -113,6 +114,7 @@ async def health_services_endpoint( # noqa: PLR0915 "langsmith", "datadog", "generic_api", + "arize", ]: raise HTTPException( status_code=400, @@ -165,6 +167,19 @@ async def health_services_endpoint( # noqa: PLR0915 "status": "success", "message": "Mock LLM request made - check langfuse.", } + elif service == "arize": + from litellm.integrations.arize.arize import ArizeLogger + + arize_logger = ArizeLogger() + response = await arize_logger.async_health_check() + return { + "status": response["status"], + "message": ( + response["error_message"] + if response["status"] == "unhealthy" + else "Arize is healthy and ready to receive traces" + ), + } if service == "webhook": user_info = CallInfo( diff --git a/ui/litellm-dashboard/src/components/callback_info_helpers.tsx b/ui/litellm-dashboard/src/components/callback_info_helpers.tsx index 66ef4d87385..595bde3fc5b 100644 --- a/ui/litellm-dashboard/src/components/callback_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/callback_info_helpers.tsx @@ -76,7 +76,7 @@ export const callbackInfo: Record = { supports_key_team_logging: true, dynamic_params: { "arize_api_key": "password", - "arize_space_id": "text", + "arize_space_key": "text", }, description: "Arize Logging Integration" }, diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index 55997ce95cd..2da70ff0aab 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -48,8 +48,10 @@ import { callback_map, callbackInfo, Callbacks, + reverse_callback_map, } from "./callback_info_helpers"; import { parseErrorMessage } from "./shared/errorUtils"; +import Image from "next/image"; interface SettingsPageProps { accessToken: string | null; userRole: string | null; @@ -107,6 +109,8 @@ const Settings: React.FC = ({ ); const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); const [callbackToDelete, setCallbackToDelete] = useState(null); + const [testingConnection, setTestingConnection] = useState(false); + const [connectionStatus, setConnectionStatus] = useState<'success' | 'error' | null>(null); useEffect(() => { if (showEditCallback && selectedEditCallback) { @@ -251,6 +255,133 @@ const Settings: React.FC = ({ } }; + const handleCallbackSelectChange = (value: string) => { + // Reset connection status when callback changes + setConnectionStatus(null); + + // Find callback by internal value - check if allCallbacks is array first + let selectedCallbackObject = null; + if (Array.isArray(allCallbacks) && allCallbacks.length > 0) { + selectedCallbackObject = allCallbacks.find( + cb => cb.litellm_callback_name === value + ); + } + + if (selectedCallbackObject) { + handleSelectedCallbackChange(selectedCallbackObject); + } else { + // Fallback: use dynamic params from callbackInfo + const displayName = reverse_callback_map[value]; + if (displayName && callbackInfo[displayName]?.dynamic_params) { + const dynamicParams = Object.keys(callbackInfo[displayName].dynamic_params); + setSelectedCallback(value); + setSelectedCallbackParams(dynamicParams); + } else { + // Final fallback: try to find in allCallbacks by index (for backward compatibility) + let legacyCallback = null; + if (Array.isArray(allCallbacks)) { + // The old system used array indices, check if value is a number + const numericValue = parseInt(value); + if (!isNaN(numericValue) && allCallbacks[numericValue]) { + legacyCallback = allCallbacks[numericValue]; + } + } + + if (legacyCallback) { + handleSelectedCallbackChange(legacyCallback); + } else { + setSelectedCallback(value); + setSelectedCallbackParams([]); + } + } + } + }; + + const getFieldType = (paramName: string, callbackName: string): "text" | "password" => { + const displayName = reverse_callback_map[callbackName]; + if (displayName && callbackInfo[displayName]?.dynamic_params) { + const paramType = callbackInfo[displayName].dynamic_params[paramName]; + return paramType === "password" ? "password" : "text"; + } + // Default heuristics for legacy callbacks + return paramName.toLowerCase().includes("key") || + paramName.toLowerCase().includes("secret") || + paramName.toLowerCase().includes("token") ? "password" : "text"; + }; + + const getFieldLabel = (paramName: string): string => { + return paramName + .replace(/_/g, " ") + .replace(/\b\w/g, l => l.toUpperCase()); + }; + + const getFieldPlaceholder = (paramName: string, callbackName: string): string => { + const displayName = reverse_callback_map[callbackName]; + + if (displayName === "Arize") { + if (paramName === "ARIZE_API_KEY" || paramName === "arize_api_key") return "Enter your Arize API Key..."; + if (paramName === "ARIZE_SPACE_KEY" || paramName === "arize_space_id") return "Enter your Arize Space Key..."; + if (paramName === "ARIZE_ENDPOINT" || paramName === "ARIZE_HTTP_ENDPOINT") return "Optional: Custom endpoint URL"; + } + + return `Enter ${getFieldLabel(paramName)}...`; + }; + + const isRequiredField = (paramName: string, callbackName: string): boolean => { + const displayName = reverse_callback_map[callbackName]; + + if (displayName === "Arize") { + return (paramName === "ARIZE_API_KEY" || paramName === "arize_api_key") || + (paramName === "ARIZE_SPACE_KEY" || paramName === "arize_space_id"); + } + + // Default: all fields are required except endpoints + return !paramName.toLowerCase().includes("endpoint") && + !paramName.toLowerCase().includes("base") && + !paramName.toLowerCase().includes("host"); + }; + + const testCallbackConnection = async () => { + if (!selectedCallback || !accessToken) return; + + setTestingConnection(true); + setConnectionStatus(null); + + try { + const formValues = addForm.getFieldsValue(); + + // Build query params for connection test + const params = new URLSearchParams(); + selectedCallbackParams.forEach(param => { + const value = formValues[param]; + if (value) { + // Convert backend param names to dynamic param names + let dynamicParamName = param.toLowerCase(); + if (selectedCallback === "arize") { + if (param === "ARIZE_API_KEY" || param === "arize_api_key") dynamicParamName = "arize_api_key"; + if (param === "ARIZE_SPACE_KEY" || param === "arize_space_id") dynamicParamName = "arize_space_id"; + } + params.append(dynamicParamName, value); + } + }); + + const response = await serviceHealthCheck(accessToken, selectedCallback); + + if (response) { + setConnectionStatus('success'); + NotificationsManager.success('Connection test successful!'); + } else { + setConnectionStatus('error'); + NotificationsManager.error('Connection test failed'); + } + } catch (error) { + setConnectionStatus('error'); + NotificationsManager.fromBackend(error); + } finally { + setTestingConnection(false); + } + }; + const handleSaveAlerts = async () => { if (!accessToken) { return; @@ -646,12 +777,7 @@ const Settings: React.FC = ({ rules={[{ required: true, message: "Please select a callback" }]} > + )} + + ); + })} + + {selectedCallback && selectedCallbackParams.length > 0 && ( +
+ {connectionStatus === 'success' && ( + + + Connection successful + + )} + {connectionStatus === 'error' && ( + + + Connection failed + + )} + { + const value = addForm.getFieldValue(param); + return isRequiredField(param, selectedCallback || "") ? value : true; + })} > - - - ))} + {testingConnection ? 'Testing...' : 'Test Connection'} + +
+ )}
Save From 3f8c4598da70cdc0e93739e698bc141ea6262344 Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Fri, 3 Oct 2025 23:37:19 -0400 Subject: [PATCH 5/6] need to remove a file This reverts commit d6c877b73ac763464f204b77135f3786342373b7. --- litellm/integrations/arize/arize.py | 88 +------ litellm/proxy/_types.py | 11 - .../health_endpoints/_health_endpoints.py | 15 -- .../src/components/callback_info_helpers.tsx | 2 +- .../src/components/settings.tsx | 221 ++---------------- 5 files changed, 22 insertions(+), 315 deletions(-) diff --git a/litellm/integrations/arize/arize.py b/litellm/integrations/arize/arize.py index 23822801b41..1d78e4cc69c 100644 --- a/litellm/integrations/arize/arize.py +++ b/litellm/integrations/arize/arize.py @@ -8,11 +8,9 @@ import os from datetime import datetime from typing import TYPE_CHECKING, Any, Optional, Union -from litellm.integrations.additional_logging_utils import AdditionalLoggingUtils from litellm.integrations.arize import _utils from litellm.integrations.opentelemetry import OpenTelemetry from litellm.types.integrations.arize import ArizeConfig -from litellm.types.integrations.base_health_check import IntegrationHealthCheckStatus from litellm.types.services import ServiceLoggerPayload from litellm.types.utils import StandardCallbackDynamicParams @@ -28,7 +26,7 @@ else: Span = Any -class ArizeLogger(OpenTelemetry, AdditionalLoggingUtils): +class ArizeLogger(OpenTelemetry): def set_attributes(self, span: Span, kwargs, response_obj: Optional[Any]): ArizeLogger.set_arize_attributes(span, kwargs, response_obj) return @@ -143,87 +141,3 @@ class ArizeLogger(OpenTelemetry, AdditionalLoggingUtils): ) return dynamic_headers - - async def async_health_check(self, standard_callback_dynamic_params: Optional[StandardCallbackDynamicParams] = None) -> IntegrationHealthCheckStatus: - """ - Check if Arize service is healthy by testing OTEL trace export - - Args: - standard_callback_dynamic_params: Dynamic parameters containing arize_api_key and arize_space_key/arize_space_id - - Returns: - IntegrationHealthCheckStatus with status and optional error message - """ - try: - api_key = None - space_key = None - - if standard_callback_dynamic_params: - api_key = standard_callback_dynamic_params.get("arize_api_key") - space_key = ( - standard_callback_dynamic_params.get("arize_space_key") or - standard_callback_dynamic_params.get("arize_space_id") # fallback for backwards compatibility - ) - - if not api_key: - api_key = os.environ.get("ARIZE_API_KEY") - if not space_key: - space_key = os.environ.get("ARIZE_SPACE_KEY") - - if not api_key or not space_key: - return IntegrationHealthCheckStatus( - status="unhealthy", - error_message="Arize credentials not configured. Please set arize_api_key and arize_space_key parameters or ARIZE_API_KEY and ARIZE_SPACE_KEY environment variables." - ) - - # Get Arize configuration - arize_config = ArizeLogger.get_arize_config() - - # Validate configuration - if not arize_config.endpoint: - return IntegrationHealthCheckStatus( - status="unhealthy", - error_message="Arize endpoint not configured. Using default endpoint https://otlp.arize.com/v1" - ) - - try: - test_headers = { - "arize-space-id": space_key.strip(), - "api_key": api_key.strip(), - } - - endpoint = arize_config.endpoint or "https://otlp.arize.com/v1" - - # For a basic health check, we just validate that the configuration is properly formed - # A full test would require actually sending a trace, which might be overkill for health checks - - return IntegrationHealthCheckStatus( - status="healthy", - error_message=None - ) - - except Exception as config_error: - return IntegrationHealthCheckStatus( - status="unhealthy", - error_message=f"Arize configuration error: {str(config_error)}" - ) - - except Exception as e: - return IntegrationHealthCheckStatus( - status="unhealthy", - error_message=f"Arize health check failed: {str(e)}" - ) - - async def get_request_response_payload( - self, - request_id: str, - start_time_utc: Optional[datetime] = None, - end_time_utc: Optional[datetime] = None, - ) -> Optional[dict]: - """ - Get the request and response payload for a given request_id from Arize. - - Note: Arize is primarily for observability/tracing, not request/response storage. - This method returns None as Arize doesn't typically store raw payloads. - """ - return None diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 292e69b6a8c..c5370eb7d70 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2368,17 +2368,6 @@ class AllCallbacks(LiteLLMPydanticObjectBase): ui_callback_name="Lago Billing", ) - arize: CallbackOnUI = CallbackOnUI( - litellm_callback_name="arize", - litellm_callback_params=[ - "ARIZE_API_KEY", - "ARIZE_SPACE_KEY", - "ARIZE_ENDPOINT", - "ARIZE_HTTP_ENDPOINT", - ], - ui_callback_name="Arize", - ) - class SpendLogsMetadata(TypedDict): """ diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 8b06b46f76e..883bff3185f 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -75,7 +75,6 @@ async def health_services_endpoint( # noqa: PLR0915 "braintrust", "datadog", "generic_api", - "arize", ], str, ] = fastapi.Query(description="Specify the service being hit."), @@ -114,7 +113,6 @@ async def health_services_endpoint( # noqa: PLR0915 "langsmith", "datadog", "generic_api", - "arize", ]: raise HTTPException( status_code=400, @@ -167,19 +165,6 @@ async def health_services_endpoint( # noqa: PLR0915 "status": "success", "message": "Mock LLM request made - check langfuse.", } - elif service == "arize": - from litellm.integrations.arize.arize import ArizeLogger - - arize_logger = ArizeLogger() - response = await arize_logger.async_health_check() - return { - "status": response["status"], - "message": ( - response["error_message"] - if response["status"] == "unhealthy" - else "Arize is healthy and ready to receive traces" - ), - } if service == "webhook": user_info = CallInfo( diff --git a/ui/litellm-dashboard/src/components/callback_info_helpers.tsx b/ui/litellm-dashboard/src/components/callback_info_helpers.tsx index 595bde3fc5b..66ef4d87385 100644 --- a/ui/litellm-dashboard/src/components/callback_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/callback_info_helpers.tsx @@ -76,7 +76,7 @@ export const callbackInfo: Record = { supports_key_team_logging: true, dynamic_params: { "arize_api_key": "password", - "arize_space_key": "text", + "arize_space_id": "text", }, description: "Arize Logging Integration" }, diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index 2da70ff0aab..55997ce95cd 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -48,10 +48,8 @@ import { callback_map, callbackInfo, Callbacks, - reverse_callback_map, } from "./callback_info_helpers"; import { parseErrorMessage } from "./shared/errorUtils"; -import Image from "next/image"; interface SettingsPageProps { accessToken: string | null; userRole: string | null; @@ -109,8 +107,6 @@ const Settings: React.FC = ({ ); const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); const [callbackToDelete, setCallbackToDelete] = useState(null); - const [testingConnection, setTestingConnection] = useState(false); - const [connectionStatus, setConnectionStatus] = useState<'success' | 'error' | null>(null); useEffect(() => { if (showEditCallback && selectedEditCallback) { @@ -255,133 +251,6 @@ const Settings: React.FC = ({ } }; - const handleCallbackSelectChange = (value: string) => { - // Reset connection status when callback changes - setConnectionStatus(null); - - // Find callback by internal value - check if allCallbacks is array first - let selectedCallbackObject = null; - if (Array.isArray(allCallbacks) && allCallbacks.length > 0) { - selectedCallbackObject = allCallbacks.find( - cb => cb.litellm_callback_name === value - ); - } - - if (selectedCallbackObject) { - handleSelectedCallbackChange(selectedCallbackObject); - } else { - // Fallback: use dynamic params from callbackInfo - const displayName = reverse_callback_map[value]; - if (displayName && callbackInfo[displayName]?.dynamic_params) { - const dynamicParams = Object.keys(callbackInfo[displayName].dynamic_params); - setSelectedCallback(value); - setSelectedCallbackParams(dynamicParams); - } else { - // Final fallback: try to find in allCallbacks by index (for backward compatibility) - let legacyCallback = null; - if (Array.isArray(allCallbacks)) { - // The old system used array indices, check if value is a number - const numericValue = parseInt(value); - if (!isNaN(numericValue) && allCallbacks[numericValue]) { - legacyCallback = allCallbacks[numericValue]; - } - } - - if (legacyCallback) { - handleSelectedCallbackChange(legacyCallback); - } else { - setSelectedCallback(value); - setSelectedCallbackParams([]); - } - } - } - }; - - const getFieldType = (paramName: string, callbackName: string): "text" | "password" => { - const displayName = reverse_callback_map[callbackName]; - if (displayName && callbackInfo[displayName]?.dynamic_params) { - const paramType = callbackInfo[displayName].dynamic_params[paramName]; - return paramType === "password" ? "password" : "text"; - } - // Default heuristics for legacy callbacks - return paramName.toLowerCase().includes("key") || - paramName.toLowerCase().includes("secret") || - paramName.toLowerCase().includes("token") ? "password" : "text"; - }; - - const getFieldLabel = (paramName: string): string => { - return paramName - .replace(/_/g, " ") - .replace(/\b\w/g, l => l.toUpperCase()); - }; - - const getFieldPlaceholder = (paramName: string, callbackName: string): string => { - const displayName = reverse_callback_map[callbackName]; - - if (displayName === "Arize") { - if (paramName === "ARIZE_API_KEY" || paramName === "arize_api_key") return "Enter your Arize API Key..."; - if (paramName === "ARIZE_SPACE_KEY" || paramName === "arize_space_id") return "Enter your Arize Space Key..."; - if (paramName === "ARIZE_ENDPOINT" || paramName === "ARIZE_HTTP_ENDPOINT") return "Optional: Custom endpoint URL"; - } - - return `Enter ${getFieldLabel(paramName)}...`; - }; - - const isRequiredField = (paramName: string, callbackName: string): boolean => { - const displayName = reverse_callback_map[callbackName]; - - if (displayName === "Arize") { - return (paramName === "ARIZE_API_KEY" || paramName === "arize_api_key") || - (paramName === "ARIZE_SPACE_KEY" || paramName === "arize_space_id"); - } - - // Default: all fields are required except endpoints - return !paramName.toLowerCase().includes("endpoint") && - !paramName.toLowerCase().includes("base") && - !paramName.toLowerCase().includes("host"); - }; - - const testCallbackConnection = async () => { - if (!selectedCallback || !accessToken) return; - - setTestingConnection(true); - setConnectionStatus(null); - - try { - const formValues = addForm.getFieldsValue(); - - // Build query params for connection test - const params = new URLSearchParams(); - selectedCallbackParams.forEach(param => { - const value = formValues[param]; - if (value) { - // Convert backend param names to dynamic param names - let dynamicParamName = param.toLowerCase(); - if (selectedCallback === "arize") { - if (param === "ARIZE_API_KEY" || param === "arize_api_key") dynamicParamName = "arize_api_key"; - if (param === "ARIZE_SPACE_KEY" || param === "arize_space_id") dynamicParamName = "arize_space_id"; - } - params.append(dynamicParamName, value); - } - }); - - const response = await serviceHealthCheck(accessToken, selectedCallback); - - if (response) { - setConnectionStatus('success'); - NotificationsManager.success('Connection test successful!'); - } else { - setConnectionStatus('error'); - NotificationsManager.error('Connection test failed'); - } - } catch (error) { - setConnectionStatus('error'); - NotificationsManager.fromBackend(error); - } finally { - setTestingConnection(false); - } - }; - const handleSaveAlerts = async () => { if (!accessToken) { return; @@ -777,7 +646,12 @@ const Settings: React.FC = ({ rules={[{ required: true, message: "Please select a callback" }]} > - )} - - ); - })} - - {selectedCallback && selectedCallbackParams.length > 0 && ( -
- {connectionStatus === 'success' && ( - - - Connection successful - - )} - {connectionStatus === 'error' && ( - - - Connection failed - - )} - { - const value = addForm.getFieldValue(param); - return isRequiredField(param, selectedCallback || "") ? value : true; - })} + selectedCallbackParams.map((param) => ( + - {testingConnection ? 'Testing...' : 'Test Connection'} - -
- )} + + + ))}
Save From 0c4aae034758a6fe928ccbcaafdef3036bf45757 Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Wed, 15 Oct 2025 22:07:16 -0400 Subject: [PATCH 6/6] fix: add arize from ui --- litellm/integrations/arize/arize.py | 34 ++- .../health_endpoints/_health_endpoints.py | 33 +-- .../arize/test_arize_health_check.py | 183 +++++++++++++ .../src/components/callback_info_helpers.tsx | 252 +++++++++--------- .../src/components/settings.tsx | 180 ++++++++----- 5 files changed, 482 insertions(+), 200 deletions(-) create mode 100644 tests/test_litellm/integrations/arize/test_arize_health_check.py diff --git a/litellm/integrations/arize/arize.py b/litellm/integrations/arize/arize.py index 1d78e4cc69c..06e05f1271d 100644 --- a/litellm/integrations/arize/arize.py +++ b/litellm/integrations/arize/arize.py @@ -103,7 +103,39 @@ class ArizeLogger(OpenTelemetry): ): """Arize is used mainly for LLM I/O tracing, sending Proxy Server Request adds bloat to arize logs""" pass - + + async def async_health_check(self): + """ + Performs a health check for Arize integration. + + Returns: + dict: Health check result with status and message + """ + try: + config = self.get_arize_config() + + if not config.space_key: + return { + "status": "unhealthy", + "error_message": "ARIZE_SPACE_KEY environment variable not set" + } + + if not config.api_key: + return { + "status": "unhealthy", + "error_message": "ARIZE_API_KEY environment variable not set" + } + + return { + "status": "healthy", + "message": "Arize credentials are configured properly" + } + + except Exception as e: + return { + "status": "unhealthy", + "error_message": f"Arize health check failed: {str(e)}" + } def construct_dynamic_otel_headers( self, diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 883bff3185f..35a80e8b7fa 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -34,7 +34,7 @@ from litellm.proxy.health_check import ( #### Health ENDPOINTS #### router = APIRouter() - +services = Union[Literal["slack_budget_alerts", "langfuse", "slack", "openmeter", "webhook", "email", "braintrust", "datadog", "generic_api", "arize"], str] @router.get( "/test", @@ -64,20 +64,7 @@ async def test_endpoint(request: Request): ) async def health_services_endpoint( # noqa: PLR0915 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), - service: Union[ - Literal[ - "slack_budget_alerts", - "langfuse", - "slack", - "openmeter", - "webhook", - "email", - "braintrust", - "datadog", - "generic_api", - ], - str, - ] = fastapi.Query(description="Specify the service being hit."), + service: services = fastapi.Query(description="Specify the service being hit."), ): """ Use this admin-only endpoint to check if the service is healthy. @@ -113,11 +100,12 @@ async def health_services_endpoint( # noqa: PLR0915 "langsmith", "datadog", "generic_api", + "arize", ]: raise HTTPException( status_code=400, detail={ - "error": f"Service must be in list. Service={service}. List={['slack_budget_alerts']}" + "error": f"Service must be in list. Service={service} not in {services}" }, ) @@ -150,6 +138,19 @@ async def health_services_endpoint( # noqa: PLR0915 else "Datadog is healthy" ), } + elif service == "arize": + from litellm.integrations.arize.arize import ArizeLogger + + arize_logger = ArizeLogger() + response = await arize_logger.async_health_check() + return { + "status": response["status"], + "message": ( + response["error_message"] + if response["status"] == "unhealthy" + else "Arize is healthy" + ), + } elif service == "langfuse": from litellm.integrations.langfuse.langfuse import LangFuseLogger diff --git a/tests/test_litellm/integrations/arize/test_arize_health_check.py b/tests/test_litellm/integrations/arize/test_arize_health_check.py new file mode 100644 index 00000000000..91d0b42d48d --- /dev/null +++ b/tests/test_litellm/integrations/arize/test_arize_health_check.py @@ -0,0 +1,183 @@ +""" +Test Arize health check functionality and proxy integration. +""" +import json +import os +import sys +from unittest.mock import patch, MagicMock + +# Adds the grandparent directory to sys.path to allow importing project modules +sys.path.insert(0, os.path.abspath("../..")) + +import asyncio +import pytest + +import litellm +from litellm.integrations.arize.arize import ArizeLogger +from litellm.types.utils import StandardCallbackDynamicParams + + +class TestArizeHealthCheck: + """Test Arize health check functionality.""" + + @pytest.mark.asyncio + async def test_arize_health_check_with_credentials(self): + """Test Arize health check returns healthy when credentials are available.""" + + with patch.dict(os.environ, { + "ARIZE_SPACE_KEY": "test-space-key", + "ARIZE_API_KEY": "test-api-key", + "ARIZE_ENDPOINT": "https://otlp.arize.com/v1" + }): + arize_logger = ArizeLogger() + response = await arize_logger.async_health_check() + + assert response["status"] == "healthy" + assert "configured properly" in response["message"] + + @pytest.mark.asyncio + async def test_arize_health_check_missing_space_key(self): + """Test Arize health check returns unhealthy when space key is missing.""" + + with patch.dict(os.environ, { + "ARIZE_API_KEY": "test-api-key" + }, clear=True): + arize_logger = ArizeLogger() + response = await arize_logger.async_health_check() + + assert response["status"] == "unhealthy" + assert "ARIZE_SPACE_KEY" in response["error_message"] + + @pytest.mark.asyncio + async def test_arize_health_check_missing_api_key(self): + """Test Arize health check returns unhealthy when API key is missing.""" + + with patch.dict(os.environ, { + "ARIZE_SPACE_KEY": "test-space-key" + }, clear=True): + arize_logger = ArizeLogger() + response = await arize_logger.async_health_check() + + assert response["status"] == "unhealthy" + assert "ARIZE_API_KEY" in response["error_message"] + + @pytest.mark.asyncio + async def test_arize_health_check_missing_both_keys(self): + """Test Arize health check when both keys are missing.""" + + with patch.dict(os.environ, {}, clear=True): + arize_logger = ArizeLogger() + response = await arize_logger.async_health_check() + + assert response["status"] == "unhealthy" + assert "ARIZE_SPACE_KEY" in response["error_message"] + + +class TestArizeIntegrationWithProxy: + """Test Arize integration with LiteLLM completion requests.""" + + @pytest.mark.asyncio + async def test_arize_logging_with_completion(self): + """Test that Arize logging works with actual completion requests.""" + + with patch.dict(os.environ, { + "ARIZE_SPACE_KEY": "test-space-key", + "ARIZE_API_KEY": "test-api-key", + "ARIZE_ENDPOINT": "https://otlp.arize.com/v1" + }): + # Create ArizeLogger instance + arize_logger = ArizeLogger() + + # Store original callbacks + original_callbacks = litellm.success_callback.copy() if litellm.success_callback else [] + + try: + # Add ArizeLogger to callbacks + litellm.success_callback = [arize_logger] + + # Make completion request + response = await litellm.acompletion( + model="openai/litellm-mock-response-model", + messages=[{"role": "user", "content": "Test message for Arize health check"}], + mock_response="This is a test response that validates Arize integration.", + user="test-arize-health" + ) + + # Verify response is valid + assert response is not None + print(f"Response type: {type(response)}") + print("✅ Arize completion request completed successfully") + + # Give time for async logging + await asyncio.sleep(0.1) + + print("✅ Arize completion logging test successful") + + finally: + # Restore original callbacks + litellm.success_callback = original_callbacks + + def test_arize_get_config(self): + """Test ArizeLogger.get_arize_config() method.""" + + with patch.dict(os.environ, { + "ARIZE_SPACE_KEY": "test-space-123", + "ARIZE_API_KEY": "test-api-456", + "ARIZE_ENDPOINT": "https://custom.arize.com/v1" + }): + config = ArizeLogger.get_arize_config() + + assert config.space_key == "test-space-123" + assert config.api_key == "test-api-456" + assert config.endpoint == "https://custom.arize.com/v1" + assert config.protocol == "otlp_grpc" + + def test_arize_get_config_defaults(self): + """Test ArizeLogger.get_arize_config() with default endpoint.""" + + with patch.dict(os.environ, { + "ARIZE_SPACE_KEY": "test-space-default", + "ARIZE_API_KEY": "test-api-default" + }, clear=True): + config = ArizeLogger.get_arize_config() + + assert config.space_key == "test-space-default" + assert config.api_key == "test-api-default" + assert config.endpoint == "https://otlp.arize.com/v1" # Default endpoint + assert config.protocol == "otlp_grpc" # Default protocol + + def test_arize_construct_dynamic_headers(self): + """Test dynamic OTEL headers construction for team/key logging.""" + + arize_logger = ArizeLogger() + + dynamic_params = StandardCallbackDynamicParams( + arize_space_key="dynamic-space-123", + arize_api_key="dynamic-api-456" + ) + + headers = arize_logger.construct_dynamic_otel_headers(dynamic_params) + + assert headers is not None + assert headers["arize-space-id"] == "dynamic-space-123" + assert headers["api_key"] == "dynamic-api-456" + + def test_arize_construct_dynamic_headers_space_id_fallback(self): + """Test dynamic headers with arize_space_id parameter (fallback).""" + + arize_logger = ArizeLogger() + + dynamic_params = StandardCallbackDynamicParams( + arize_space_id="fallback-space-789", # Using space_id instead of space_key + arize_api_key="fallback-api-999" + ) + + headers = arize_logger.construct_dynamic_otel_headers(dynamic_params) + + assert headers is not None + assert headers["arize-space-id"] == "fallback-space-789" + assert headers["api_key"] == "fallback-api-999" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/callback_info_helpers.tsx b/ui/litellm-dashboard/src/components/callback_info_helpers.tsx index 66ef4d87385..5c13d2a095a 100644 --- a/ui/litellm-dashboard/src/components/callback_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/callback_info_helpers.tsx @@ -1,137 +1,151 @@ -export enum Callbacks { - Braintrust = "Braintrust", - CustomCallbackAPI = "Custom Callback API", - Datadog = "Datadog", - Langfuse = "Langfuse", - LangfuseOtel = "LangfuseOtel", - LangSmith = "LangSmith", - Lago = "Lago", - OpenMeter = "OpenMeter", - OTel = "Open Telemetry", - S3 = "S3", - Arize = "Arize", -} - -export const callback_map: Record = { - Braintrust: "braintrust", - CustomCallbackAPI: "custom_callback_api", - Datadog: "datadog", - Langfuse: "langfuse", - LangfuseOtel: "langfuse_otel", - LangSmith: "langsmith", - Lago: "lago", - OpenMeter: "openmeter", - OTel: "otel", - S3: "s3", - Arize: "arize", -} - -// Reverse mapping from internal values to display names -export const reverse_callback_map: Record = Object.fromEntries( - Object.entries(callback_map).map(([key, value]) => [value, key]) -); - -// Utility function to convert internal callback values to display names -export const mapInternalToDisplayNames = (internalValues: string[]): string[] => { - return internalValues.map(value => reverse_callback_map[value] || value); -}; - -// Utility function to convert display names to internal callback values -export const mapDisplayToInternalNames = (displayValues: string[]): string[] => { - return displayValues.map(value => callback_map[value] || value); -}; - const asset_logos_folder = '/ui/assets/logos/'; -interface CallbackInfo { - logo: string; +interface CallbackConfig { + id: string; // Internal callback name (e.g., "arize", "custom_callback_api") + displayName: string; // User-facing name (e.g., "Arize", "Custom Callback API") + logo: string; // Logo path supports_key_team_logging: boolean; dynamic_params: Record; - description: string | null; + description: string; } -export const callbackInfo: Record = { - [Callbacks.Langfuse]: { +// Single source of truth for ALL callback configurations +export const CALLBACK_CONFIGS: CallbackConfig[] = [ + { + id: "arize", + displayName: "Arize", + logo: `${asset_logos_folder}arize.png`, + supports_key_team_logging: true, + dynamic_params: { + "arize_api_key": "password", + "arize_space_key": "password", + }, + description: "Arize Logging Integration" + }, + { + id: "braintrust", + displayName: "Braintrust", + logo: `${asset_logos_folder}braintrust.png`, + supports_key_team_logging: false, + dynamic_params: { + "braintrust_api_key": "password", + "braintrust_project_name": "text" + }, + description: "Braintrust Logging Integration" + }, + { + id: "custom_callback_api", + displayName: "Custom Callback API", + logo: `${asset_logos_folder}custom.svg`, + supports_key_team_logging: true, + dynamic_params: { + "custom_callback_api_url": "text", + "custom_callback_api_headers": "text" + }, + description: "Custom Callback API Logging Integration" + }, + { + id: "datadog", + displayName: "Datadog", + logo: `${asset_logos_folder}datadog.png`, + supports_key_team_logging: false, + dynamic_params: { + "dd_api_key": "password", + "dd_site": "text" + }, + description: "Datadog Logging Integration" + }, + { + id: "lago", + displayName: "Lago", + logo: `${asset_logos_folder}lago.svg`, + supports_key_team_logging: false, + dynamic_params: { + "lago_api_url": "text", + "lago_api_key": "password" + }, + description: "Lago Billing Logging Integration" + }, + { + id: "langfuse", + displayName: "Langfuse", logo: `${asset_logos_folder}langfuse.png`, supports_key_team_logging: true, dynamic_params: { - "langfuse_public_key": "text", - "langfuse_secret_key": "password", - "langfuse_host": "text" + "langfuse_public_key": "text", + "langfuse_secret_key": "password", + "langfuse_host": "text" }, description: "Langfuse v2 Logging Integration" + }, + { + id: "langfuse_otel", + displayName: "Langfuse OTEL", + logo: `${asset_logos_folder}langfuse.png`, + supports_key_team_logging: true, + dynamic_params: { + "langfuse_public_key": "text", + "langfuse_secret_key": "password", + "langfuse_host": "text" }, - [Callbacks.LangfuseOtel]: { - logo: `${asset_logos_folder}langfuse.png`, - supports_key_team_logging: true, - dynamic_params: { - "langfuse_public_key": "text", - "langfuse_secret_key": "password", - "langfuse_host": "text" - }, - description: "Langfuse v3 OTEL Logging Integration" + description: "Langfuse v3 OTEL Logging Integration" + }, + { + id: "langsmith", + displayName: "LangSmith", + logo: `${asset_logos_folder}langsmith.png`, + supports_key_team_logging: true, + dynamic_params: { + "langsmith_api_key": "password", + "langsmith_project": "text", + "langsmith_base_url": "text", + "langsmith_sampling_rate": "number" }, - [Callbacks.Arize]: { - logo: `${asset_logos_folder}arize.png`, - supports_key_team_logging: true, - dynamic_params: { - "arize_api_key": "password", - "arize_space_id": "text", - }, - description: "Arize Logging Integration" + description: "Langsmith Logging Integration" + }, + { + id: "openmeter", + displayName: "OpenMeter", + logo: `${asset_logos_folder}openmeter.png`, + supports_key_team_logging: false, + dynamic_params: { + "openmeter_api_key": "password", + "openmeter_base_url": "text" }, - [Callbacks.LangSmith]: { - logo: `${asset_logos_folder}langsmith.png`, - supports_key_team_logging: true, - dynamic_params: { - "langsmith_api_key": "password", - "langsmith_project": "text", - "langsmith_base_url": "text", - "langsmith_sampling_rate": "number" - }, - description: "Langsmith Logging Integration" + description: "OpenMeter Logging Integration" + }, + { + id: "otel", + displayName: "Open Telemetry", + logo: `${asset_logos_folder}otel.png`, + supports_key_team_logging: false, + dynamic_params: { + "otel_endpoint": "text", + "otel_headers": "text" }, - [Callbacks.Braintrust]: { - logo: `${asset_logos_folder}braintrust.png`, - supports_key_team_logging: false, - dynamic_params: {}, - description: "Braintrust Logging Integration" + description: "OpenTelemetry Logging Integration" + }, + { + id: "s3", + displayName: "S3", + logo: `${asset_logos_folder}aws.svg`, + supports_key_team_logging: false, + dynamic_params: { + "s3_bucket_name": "text", + "aws_access_key_id": "password", + "aws_secret_access_key": "password", + "aws_region": "text" }, - [Callbacks.CustomCallbackAPI]: { - logo: `${asset_logos_folder}custom.svg`, - supports_key_team_logging: true, - dynamic_params: {}, - description: "Custom Callback API Logging Integration" - }, - [Callbacks.Datadog]: { - logo: `${asset_logos_folder}datadog.png`, - supports_key_team_logging: false, - dynamic_params: {}, - description: "Datadog Logging Integration" - }, - [Callbacks.Lago]: { - logo: `${asset_logos_folder}lago.svg`, - supports_key_team_logging: false, - dynamic_params: {}, - description: "Lago Billing Logging Integration" - }, - [Callbacks.OpenMeter]: { - logo: `${asset_logos_folder}openmeter.png`, - supports_key_team_logging: false, - dynamic_params: {}, - description: "OpenMeter Logging Integration" - }, - [Callbacks.OTel]: { - logo: `${asset_logos_folder}otel.png`, - supports_key_team_logging: false, - dynamic_params: {}, - description: "OpenTelemetry Logging Integration" - }, - [Callbacks.S3]: { - logo: `${asset_logos_folder}aws.svg`, - supports_key_team_logging: false, - dynamic_params: {}, - description: "S3 Bucket (AWS) Logging Integration" - } + description: "S3 Bucket (AWS) Logging Integration" + } +]; + +// Utility functions for easy access +export const getCallbackById = (id: string): CallbackConfig | undefined => { + return CALLBACK_CONFIGS.find(callback => callback.id === id); +}; + +export const getCallbackByDisplayName = (displayName: string): CallbackConfig | undefined => { + return CALLBACK_CONFIGS.find(callback => callback.displayName === displayName); }; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index 55997ce95cd..752bce3b5a8 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -45,9 +45,8 @@ import { import AlertingSettings from "./alerting/alerting_settings"; import FormItem from "antd/es/form/FormItem"; import { - callback_map, - callbackInfo, - Callbacks, + CALLBACK_CONFIGS, + getCallbackById, } from "./callback_info_helpers"; import { parseErrorMessage } from "./shared/errorUtils"; interface SettingsPageProps { @@ -239,18 +238,21 @@ const Settings: React.FC = ({ } }; - const handleSelectedCallbackChange = ( - callbackObject: genericCallbackParams - ) => { - setSelectedCallback(callbackObject.litellm_callback_name); - - if (callbackObject && callbackObject.litellm_callback_params) { - setSelectedCallbackParams(callbackObject.litellm_callback_params); + const handleSelectedCallbackChange = (callbackName: string) => { + setSelectedCallback(callbackName); + + // Get the callback configuration using the new clean structure + const callbackConfig = getCallbackById(callbackName); + + // Get the parameters from the callback configuration + if (callbackConfig?.dynamic_params) { + const params = Object.keys(callbackConfig.dynamic_params); + setSelectedCallbackParams(params); } else { setSelectedCallbackParams([]); } }; - + const handleSaveAlerts = async () => { if (!accessToken) { return; @@ -639,74 +641,124 @@ const Settings: React.FC = ({ wrapperCol={{ span: 16 }} labelAlign="left" > - <> - {selectedCallbackParams && - selectedCallbackParams.map((param) => ( - - - - ))} + {selectedCallbackParams && selectedCallbackParams.length > 0 && ( +
+ {selectedCallbackParams.map((param) => { + // Get the callback configuration to look up parameter types + const callbackConfig = getCallbackById(selectedCallback || ''); + const paramType = callbackConfig?.dynamic_params[param] || "text"; + + const fieldLabel = param.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase()); + + return ( + + {fieldLabel} + * + + } + name={param} + key={param} + className="mb-4" + rules={[ + { + required: true, + message: `Please enter the ${fieldLabel.toLowerCase()}`, + }, + ]} + > + {paramType === "password" ? ( + + ) : paramType === "number" ? ( + + ) : ( + + )} + + ); + })} +
+ )} -
- Save +
+ + + Add Callback +
-