diff --git a/README.md b/README.md index 528dd53581..47878747a6 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Support for more providers. Missing a provider or LLM Platform, raise a [feature # Usage ([**Docs**](https://docs.litellm.ai/docs/)) > [!IMPORTANT] -> LiteLLM v1.0.0 now requires `openai>=1.0.0`. Migration guide [here](https://docs.litellm.ai/docs/migration) +> LiteLLM v1.0.0 now requires `openai>=1.0.0`. Migration guide [here](https://docs.litellm.ai/docs/migration) > LiteLLM v1.40.14+ now requires `pydantic>=2.0.0`. No changes required. @@ -132,7 +132,7 @@ print(response) ## Streaming ([Docs](https://docs.litellm.ai/docs/completion/stream)) -liteLLM supports streaming the model response back, pass `stream=True` to get a streaming iterator in response. +liteLLM supports streaming the model response back, pass `stream=True` to get a streaming iterator in response. Streaming is supported for all models (Bedrock, Huggingface, TogetherAI, Azure, OpenAI, etc.) ```python @@ -234,7 +234,7 @@ $ litellm --model huggingface/bigcode/starcoder > [!IMPORTANT] -> 💡 [Use LiteLLM Proxy with Langchain (Python, JS), OpenAI SDK (Python, JS) Anthropic SDK, Mistral SDK, LlamaIndex, Instructor, Curl](https://docs.litellm.ai/docs/proxy/user_keys) +> 💡 [Use LiteLLM Proxy with Langchain (Python, JS), OpenAI SDK (Python, JS) Anthropic SDK, Mistral SDK, LlamaIndex, Instructor, Curl](https://docs.litellm.ai/docs/proxy/user_keys) ```python import openai # openai v1.0.0+ @@ -266,7 +266,7 @@ echo 'LITELLM_MASTER_KEY="sk-1234"' > .env # Add the litellm salt key - you cannot change this after adding a model # It is used to encrypt / decrypt your LLM API Key credentials -# We recommend - https://1password.com/password-generator/ +# We recommend - https://1password.com/password-generator/ # password generator to get a random hash for litellm salt key echo 'LITELLM_SALT_KEY="sk-1234"' >> .env @@ -340,6 +340,7 @@ curl 'http://0.0.0.0:4000/key/generate' \ | [xinference [Xorbits Inference]](https://docs.litellm.ai/docs/providers/xinference) | | | | | ✅ | | | [FriendliAI](https://docs.litellm.ai/docs/providers/friendliai) | ✅ | ✅ | ✅ | ✅ | | | | [Galadriel](https://docs.litellm.ai/docs/providers/galadriel) | ✅ | ✅ | ✅ | ✅ | | | +| [GradientAI](https://docs.litellm.ai/docs/providers/gradient_ai) | ✅ | ✅ | | | | | | [Novita AI](https://novita.ai/models/llm?utm_source=github_litellm&utm_medium=github_readme&utm_campaign=github_link) | ✅ | ✅ | ✅ | ✅ | | | | [Featherless AI](https://docs.litellm.ai/docs/providers/featherless_ai) | ✅ | ✅ | ✅ | ✅ | | | | [Nebius AI Studio](https://docs.litellm.ai/docs/providers/nebius) | ✅ | ✅ | ✅ | ✅ | ✅ | | @@ -348,7 +349,7 @@ curl 'http://0.0.0.0:4000/key/generate' \ ## Contributing -Interested in contributing? Contributions to LiteLLM Python SDK, Proxy Server, and LLM integrations are both accepted and highly encouraged! +Interested in contributing? Contributions to LiteLLM Python SDK, Proxy Server, and LLM integrations are both accepted and highly encouraged! **Quick start:** `git clone` → `make install-dev` → `make format` → `make lint` → `make test-unit` @@ -359,7 +360,7 @@ For companies that need better security, user management and professional suppor [Talk to founders](https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat) -This covers: +This covers: - ✅ **Features under the [LiteLLM Commercial License](https://docs.litellm.ai/docs/proxy/enterprise):** - ✅ **Feature Prioritization** - ✅ **Custom Integrations** diff --git a/docs/my-website/docs/providers/gradient_ai.md b/docs/my-website/docs/providers/gradient_ai.md new file mode 100644 index 0000000000..7b5eef04dc --- /dev/null +++ b/docs/my-website/docs/providers/gradient_ai.md @@ -0,0 +1,79 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# GradientAI +https://digitalocean.com/products/gradientai + + +LiteLLM provides native support for GradientAI models. +To use a GradientAI model, specify it as `gradient_ai/` in your LiteLLM requests. + + +## API Key & Endpoint + +Set your credentials and endpoint as environment variables: + +```python +import os +os.environ['GRADIENT_AI_API_KEY'] = "your-api-key" +os.environ['GRADIENT_AI_AGENT_ENDPOINT'] = "https://api.gradient_ai.com/api/v1/chat" # default endpoint +``` + +## Sample Usage + +```python +from litellm import completion +import os + +os.environ['GRADIENT_AI_API_KEY'] = "your-api-key" +response = completion( + model="gradient_ai/model-name", + messages=[ + {"role": "user", "content": "Hello, how are you?"} + ], +) +print(response.choices[0].message.content) +``` + +## Streaming Example + +```python +from litellm import completion +import os + +os.environ['GRADIENT_AI_API_KEY'] = "your-api-key" +response = completion( + model="gradient_ai/model-name", + messages=[ + {"role": "user", "content": "Write a story about a robot learning to love"} + ], + stream=True, +) + +for chunk in response: + print(chunk.choices[0].delta.content or "", end="") +``` + +## Supported Parameters + +| Parameter | Type | Description | +|-----------------------------------|--------------|--------------------------------------------------------------------| +| `temperature` | float | Controls randomness (0.0-2.0) | +| `top_p` | float | Nucleus sampling parameter (0.0-1.0) | +| `max_tokens` | int | Maximum tokens to generate | +| `max_completion_tokens` | int | Alternative to max_tokens | +| `stream` | bool | Whether to stream the response | +| `k` | int | Top results to return from knowledge bases | +| `retrieval_method` | string | Retrieval strategy (rewrite/step_back/sub_queries/none) | +| `frequency_penalty` | float | Penalizes repeated tokens (-2.0 to 2.0) | +| `presence_penalty` | float | Penalizes tokens based on presence (-2.0 to 2.0) | +| `stop` | string/list | Sequences to stop generation | +| `kb_filters` | List[Dict] | Filters for knowledge base retrieval | +| `instruction_override` | string | Override agent's default instruction | +| `include_retrieval_info` | bool | Include document retrieval metadata | +| `include_guardrails_info` | bool | Include guardrail trigger metadata | +| `provide_citations` | bool | Include citations in response | + +--- + +For more details, see [DigitalOcean GradientAI documentation](https://digitalocean.com/products/gradientai). \ No newline at end of file diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 14dc2a6252..419afcd546 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -82,12 +82,12 @@ const sidebars = { "tutorials/cost_tracking_coding", ] }, - + ], // But you can create a sidebar manually tutorialSidebar: [ { type: "doc", id: "index" }, // NEW - + { type: "category", label: "LiteLLM Proxy Server", @@ -214,7 +214,7 @@ const sidebars = { "proxy/dynamic_logging" ], }, - + { type: "category", label: "Secret Managers", @@ -467,6 +467,7 @@ const sidebars = { "providers/custom_llm_server", "providers/petals", "providers/snowflake", + "providers/gradient_ai", "providers/featherless_ai", "providers/nebius", "providers/dashscope", @@ -505,7 +506,7 @@ const sidebars = { ] }, - + { type: "category", label: "Routing, Loadbalancing & Fallbacks", @@ -536,7 +537,7 @@ const sidebars = { }, ], }, - + { type: "category", label: "Load Testing", diff --git a/litellm/__init__.py b/litellm/__init__.py index bb53fd3a4d..d1f0648889 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -231,6 +231,7 @@ aleph_alpha_key: Optional[str] = None nlp_cloud_key: Optional[str] = None novita_api_key: Optional[str] = None snowflake_key: Optional[str] = None +gradient_ai_api_key: Optional[str] = None nebius_key: Optional[str] = None common_cloud_provider_auth_params: dict = { "params": ["project", "region_name", "token"], @@ -520,6 +521,7 @@ sambanova_models: List = [] novita_models: List = [] assemblyai_models: List = [] snowflake_models: List = [] +gradient_ai_models: List = [] llama_models: List = [] nscale_models: List = [] nebius_models: List = [] @@ -703,6 +705,8 @@ def add_known_models(): jina_ai_models.append(key) elif value.get("litellm_provider") == "snowflake": snowflake_models.append(key) + elif value.get("litellm_provider") == "gradient_ai": + gradient_ai_models.append(key) elif value.get("litellm_provider") == "featherless_ai": featherless_ai_models.append(key) elif value.get("litellm_provider") == "deepgram": @@ -802,6 +806,7 @@ model_list = ( + assemblyai_models + jina_ai_models + snowflake_models + + gradient_ai_models + llama_models + featherless_ai_models + nscale_models @@ -875,6 +880,7 @@ models_by_provider: dict = { "assemblyai": assemblyai_models, "jina_ai": jina_ai_models, "snowflake": snowflake_models, + "gradient_ai": gradient_ai_models, "meta_llama": llama_models, "nscale": nscale_models, "featherless_ai": featherless_ai_models, @@ -1141,7 +1147,7 @@ from .llms.openai.chat.o_series_transformation import ( ) from .llms.snowflake.chat.transformation import SnowflakeConfig - +from .llms.gradient_ai.chat.transformation import GradientAIConfig openaiOSeriesConfig = OpenAIOSeriesConfig() from .llms.openai.chat.gpt_transformation import ( OpenAIGPTConfig, diff --git a/litellm/constants.py b/litellm/constants.py index c7404f10a7..61a4af41be 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -270,6 +270,7 @@ LITELLM_CHAT_PROVIDERS = [ "llamafile", "lm_studio", "galadriel", + "gradient_ai", "github_copilot", # GitHub Copilot Chat API "novita", "meta_llama", diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 702196a7f0..cc39b7b590 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -351,6 +351,8 @@ def get_llm_provider( # noqa: PLR0915 custom_llm_provider = "openai" elif model in litellm.empower_models: custom_llm_provider = "empower" + elif model in litellm.gradient_ai_models: + custom_llm_provider = "gradient_ai" elif model == "*": custom_llm_provider = "openai" # bytez models @@ -664,6 +666,13 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915 or f"https://{get_secret('SNOWFLAKE_ACCOUNT_ID')}.snowflakecomputing.com/api/v2/cortex/inference:complete" ) # type: ignore dynamic_api_key = api_key or get_secret_str("SNOWFLAKE_JWT") + elif custom_llm_provider == "gradient_ai": + ( + api_base, + dynamic_api_key, + ) = litellm.GradientAIConfig()._get_openai_compatible_provider_info( + api_base, api_key + ) elif custom_llm_provider == "featherless_ai": ( api_base, diff --git a/litellm/llms/gradient_ai/chat/transformation.py b/litellm/llms/gradient_ai/chat/transformation.py new file mode 100644 index 0000000000..d631affdef --- /dev/null +++ b/litellm/llms/gradient_ai/chat/transformation.py @@ -0,0 +1,147 @@ +from typing import List, Optional, Tuple, Union, Dict, Literal + +from litellm.secret_managers.main import get_secret_str +from litellm.types.llms.openai import ( + AllMessageValues, +) + +from ...openai_like.chat.transformation import OpenAILikeChatConfig + +# Default GradientAI endpoint +GRADIENT_AI_SERVERLESS_ENDPOINT = "https://inference.do-ai.run" + + +class GradientAIConfig(OpenAILikeChatConfig): + + k: Optional[int] = None + kb_filters: Optional[List[Dict]] = None + filter_kb_content_by_query_metadata: Optional[bool] = None + instruction_override: Optional[str] = None + include_functions_info: Optional[bool] = None + include_retrieval_info: Optional[bool] = None + include_guardrails_info: Optional[bool] = None + provide_citations: Optional[bool] = None + retrieval_method: Optional[Literal["rewrite", "step_back", "sub_queries", "none"]] = None + + def __init__( + self, + frequency_penalty: Optional[float] = None, + max_tokens: Optional[int] = None, + max_completion_tokens: Optional[int] = None, + presence_penalty: Optional[float] = None, + retrieval_method: Optional[str] = None, + stop: Optional[Union[str, List[str]]] = None, + stream: Optional[bool] = None, + temperature: Optional[float] = None, + top_p: Optional[float] = None, + k: Optional[int] = None, + kb_filters: Optional[List[Dict]] = None, + filter_kb_content_by_query_metadata: Optional[bool] = None, + instruction_override: Optional[str] = None, + include_functions_info: Optional[bool] = None, + include_retrieval_info: Optional[bool] = None, + include_guardrails_info: Optional[bool] = None, + provide_citations: Optional[bool] = None, + ) -> None: + locals_ = locals().copy() + for key, value in locals_.items(): + if key != "self" and value is not None: + setattr(self.__class__, key, value) + + @classmethod + def get_config(cls): + return super().get_config() + + def get_supported_openai_params(self, model: str) -> list: + supported_params = [ + "frequency_penalty", + "max_tokens", + "max_completion_tokens", + "presence_penalty", + "stop", + "stream", + "stream_options", + "temperature", + "top_p", + # GradientAI specific parameters + "k", + "kb_filters", + "filter_kb_content_by_query_metadata", + "instruction_override", + "include_functions_info", + "include_retrieval_info", + "include_guardrails_info", + "provide_citations", + "retrieval_method", + ] + return supported_params + + def validate_environment(self, + headers: dict, + model: str, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + api_key: Optional[str] = None, + api_base: Optional[str] = None): + api_key = api_key or get_secret_str("GRADIENT_AI_API_KEY") + if api_key is None: + raise ValueError("GradientAI API key not found") + if headers is None: + headers = {} + headers["Authorization"] = f"Bearer {api_key}" + headers["Content-Type"] = "application/json" + return headers + + def get_complete_url( + self, + api_base: Optional[str], + api_key: Optional[str], + model: str, + optional_params: dict, + litellm_params: dict, + stream: Optional[bool] = None, + ) -> str: + gradient_ai_endpoint = get_secret_str("GRADIENT_AI_AGENT_ENDPOINT") + complete_url = f"{GRADIENT_AI_SERVERLESS_ENDPOINT}/v1/chat/completions" + + if api_base and api_base != GRADIENT_AI_SERVERLESS_ENDPOINT: + complete_url = f"{api_base}/api/v1/chat/completions" + elif gradient_ai_endpoint and gradient_ai_endpoint != GRADIENT_AI_SERVERLESS_ENDPOINT: + complete_url = f"{gradient_ai_endpoint}/api/v1/chat/completions" + + return complete_url + + def _get_openai_compatible_provider_info( + self, api_base: Optional[str], api_key: Optional[str] + ) -> Tuple[Optional[str], Optional[str]]: + gradient_ai_endpoint = get_secret_str("GRADIENT_AI_AGENT_ENDPOINT") + + if not api_base and not gradient_ai_endpoint: + api_base = GRADIENT_AI_SERVERLESS_ENDPOINT + else: + api_base = api_base or gradient_ai_endpoint + + dynamic_api_key = api_key or get_secret_str("GRADIENT_AI_API_KEY") + return api_base, dynamic_api_key + + def map_openai_params( + self, + non_default_params: dict, + optional_params: dict, + model: str, + drop_params: bool = False, + replace_max_completion_tokens_with_max_tokens: bool = False, + ) -> dict: + supported_openai_params = self.get_supported_openai_params(model=model) + for param, value in non_default_params.items(): + if param in supported_openai_params: + optional_params[param] = value + elif not drop_params: + from litellm.utils import UnsupportedParamsError + raise UnsupportedParamsError( + status_code=400, + message=f"GradientAI does not support parameter '{param}'. To drop unsupported params, set `drop_params=True`." + ) + + return optional_params diff --git a/litellm/main.py b/litellm/main.py index 124652fcb5..e2479b073c 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -3303,6 +3303,25 @@ def completion( # type: ignore # noqa: PLR0915 additional_args={"headers": headers}, ) raise e + elif custom_llm_provider == "gradient_ai": + + api_base = litellm.api_base or api_base + response = base_llm_http_handler.completion( + model=model, + stream=stream, + messages=messages, + acompletion=acompletion, + api_base=api_base, + model_response=model_response, + optional_params=optional_params, + litellm_params=litellm_params, + custom_llm_provider="gradient_ai", + timeout=timeout, + headers=headers, + encoding=encoding, + api_key=api_key, + logging_obj=logging, + ) elif custom_llm_provider == "bytez": api_key = ( diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 75c7d28460..e68a9b9ae3 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -1618,7 +1618,7 @@ class ImageResponse(OpenAIImageResponse, BaseLiteLLMOpenAIResponseObject): usage: Optional[ImageUsage] = None # type: ignore """ - Users might use litellm with older python versions, we don't want this to break for them. + Users might use litellm with older python versions, we don't want this to break for them. Happens when their OpenAIImageResponse has the old OpenAI usage class. """ @@ -2324,6 +2324,7 @@ class LlmProviders(str, Enum): ASSEMBLYAI = "assemblyai" GITHUB_COPILOT = "github_copilot" SNOWFLAKE = "snowflake" + GRADIENT_AI = "gradient_ai" LLAMA = "meta_llama" NSCALE = "nscale" PG_VECTOR = "pg_vector" diff --git a/litellm/utils.py b/litellm/utils.py index 667625b68b..17d10b04b6 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -6963,6 +6963,8 @@ class ProviderConfigManager: return litellm.LiteLLMProxyChatConfig() elif litellm.LlmProviders.OPENAI == provider: return litellm.OpenAIGPTConfig() + elif litellm.LlmProviders.GRADIENT_AI == provider: + return litellm.GradientAIConfig() elif litellm.LlmProviders.NSCALE == provider: return litellm.NscaleConfig() elif litellm.LlmProviders.OCI == provider: diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 1bd7b460d6..7cfc9dea5a 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -17098,6 +17098,130 @@ "litellm_provider": "snowflake", "mode": "chat" }, + "gradient_ai/anthropic-claude-3.7-sonnet": { + "input_cost_per_token": 3e-06, + "output_cost_per_token": 15e-06, + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 1024, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, + "gradient_ai/anthropic-claude-3.5-sonnet": { + "input_cost_per_token": 3e-06, + "output_cost_per_token": 15e-06, + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 1024, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, + "gradient_ai/anthropic-claude-3.5-haiku": { + "input_cost_per_token": 8e-07, + "output_cost_per_token": 4e-06, + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 1024, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, + "gradient_ai/anthropic-claude-3-opus": { + "input_cost_per_token": 15e-06, + "output_cost_per_token": 75e-06, + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 1024, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, + "gradient_ai/deepseek-r1-distill-llama-70b": { + "input_cost_per_token": 99e-08, + "output_cost_per_token": 99e-08, + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 8000, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, + "gradient_ai/llama3.3-70b-instruct": { + "input_cost_per_token": 65e-08, + "output_cost_per_token": 65e-08, + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 2048, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, + "gradient_ai/llama3-8b-instruct": { + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 512, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, + "gradient_ai/mistral-nemo-instruct-2407": { + "input_cost_per_token": 3e-07, + "output_cost_per_token": 3e-07, + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 512, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, + "gradient_ai/openai-o3": { + "input_cost_per_token": 2e-06, + "output_cost_per_token": 8e-06, + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 100000, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, + "gradient_ai/openai-o3-mini": { + "input_cost_per_token": 11e-07, + "output_cost_per_token": 44e-07, + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 100000, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, + "gradient_ai/openai-gpt-4o": { + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 16384, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, + "gradient_ai/openai-gpt-4o-mini": { + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 16384, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, + "gradient_ai/alibaba-qwen3-32b": { + "litellm_provider": "gradient_ai", + "mode": "chat", + "max_tokens": 2048, + "supported_endpoints": ["/v1/chat/completions"], + "supported_modalities": ["text"], + "supports_tool_choice": false + }, "nscale/meta-llama/Llama-4-Scout-17B-16E-Instruct": { "input_cost_per_token": 9e-08, "output_cost_per_token": 2.9e-07, diff --git a/tests/litellm/llms/gradient_ai/chat/test_gradient_ai_chat_transformation.py b/tests/litellm/llms/gradient_ai/chat/test_gradient_ai_chat_transformation.py new file mode 100644 index 0000000000..66b4b36fcd --- /dev/null +++ b/tests/litellm/llms/gradient_ai/chat/test_gradient_ai_chat_transformation.py @@ -0,0 +1,91 @@ +import os +import sys +import pytest + +sys.path.insert( + 0, os.path.abspath("../../../../..") +) # Adds the parent directory to the system path + +from litellm.llms.gradient_ai.chat.transformation import GradientAIConfig, GRADIENT_AI_SERVERLESS_ENDPOINT + +DO_ENDPOINT_PATH = "/api/v1/chat/completions" +DO_BASE_URL = "https://api.gradient_ai.com" + +@pytest.fixture +def config(): + return GradientAIConfig() + +def test_validate_environment_sets_headers(monkeypatch, config): + monkeypatch.setenv("GRADIENT_AI_API_KEY", "test-key") + headers = {} + result = config.validate_environment( + headers=headers, + model="gradient_ai/test-model", + messages=[], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + assert result["Authorization"] == "Bearer test-key" + assert result["Content-Type"] == "application/json" + +def test_get_complete_url_custom_base(config): + url = config.get_complete_url( + api_base=DO_BASE_URL, + api_key="test-key", + model="gradient_ai/test-model", + optional_params={}, + litellm_params={}, + stream=False, + ) + assert url == f"{DO_BASE_URL}{DO_ENDPOINT_PATH}" + +def test_get_complete_url_default_serverless(monkeypatch, config): + monkeypatch.delenv("GRADIENT_AI_AGENT_ENDPOINT", raising=False) + url = config.get_complete_url( + api_base=None, + api_key="test-key", + model="gradient_ai/test-model", + optional_params={}, + litellm_params={}, + stream=False, + ) + assert url == f"{GRADIENT_AI_SERVERLESS_ENDPOINT}/v1/chat/completions" + +def test_get_complete_url_with_env_endpoint(monkeypatch, config): + monkeypatch.setenv("GRADIENT_AI_AGENT_ENDPOINT", DO_BASE_URL) + url = config.get_complete_url( + api_base=None, + api_key="test-key", + model="gradient_ai/test-model", + optional_params={}, + litellm_params={}, + stream=False, + ) + assert url == f"{DO_BASE_URL}{DO_ENDPOINT_PATH}" + +def test_transform_messages_handles_dicts_only(config): + messages = [ + {"role": "assistant", "content": "Hello!"}, + {"role": "user", "content": "Hi!"}, + ] + out = config._transform_messages(messages, model="gradient_ai/test-model") + assert out[0]["role"] == "assistant" + assert out[0]["content"] == "Hello!" + assert out[1]["role"] == "user" + assert out[1]["content"] == "Hi!" + +def test_get_openai_compatible_provider_info_env(monkeypatch, config): + monkeypatch.setenv("GRADIENT_AI_AGENT_ENDPOINT", DO_BASE_URL) + monkeypatch.setenv("GRADIENT_AI_API_KEY", "env-key") + api_base, api_key = config._get_openai_compatible_provider_info(None, None) + assert api_base == DO_BASE_URL + assert api_key == "env-key" + +def test_get_openai_compatible_provider_info_default(monkeypatch, config): + monkeypatch.delenv("GRADIENT_AI_AGENT_ENDPOINT", raising=False) + monkeypatch.setenv("GRADIENT_AI_API_KEY", "env-key") + api_base, api_key = config._get_openai_compatible_provider_info(None, None) + assert api_base == GRADIENT_AI_SERVERLESS_ENDPOINT + assert api_key == "env-key" \ No newline at end of file diff --git a/ui/litellm-dashboard/out/assets/logos/gradientai.svg b/ui/litellm-dashboard/out/assets/logos/gradientai.svg new file mode 100644 index 0000000000..7e99cdda8f --- /dev/null +++ b/ui/litellm-dashboard/out/assets/logos/gradientai.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx b/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx index 8d910bc130..d74283ec97 100644 --- a/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx +++ b/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx @@ -374,6 +374,20 @@ const PROVIDER_CREDENTIAL_FIELDS: Record = type: "password", required: true }], + [Providers.GradientAI]: [ + { + key: "api_base", + label: "GradientAI Endpoint", + placeholder: "https://...", + required: false + }, + { + key: "api_key", + label: "GradientAI API Key", + type: "password", + required: true + } + ], [Providers.Triton]: [{ key: "api_key", label: "API Key", @@ -446,7 +460,7 @@ const ProviderSpecificFields: React.FC = ({ onChange(info: any) { console.log("Upload onChange triggered in ProviderSpecificFields"); console.log("Current form values:", form.getFieldsValue()); - + if (info.file.status !== "uploading") { console.log(info.file, info.fileList); } @@ -465,7 +479,7 @@ const ProviderSpecificFields: React.FC = ({ className={field.key === "vertex_credentials" ? "mb-0" : undefined} > {field.type === "select" ? ( - ) : field.type === "upload" ? ( - { // First call the original onChange if (uploadProps?.onChange) { uploadProps.onChange(info); } - + // Check the field value after a short delay setTimeout(() => { const value = form.getFieldValue(field.key); @@ -494,9 +508,9 @@ const ProviderSpecificFields: React.FC = ({ }>Click to Upload ) : ( - )} @@ -536,4 +550,4 @@ const ProviderSpecificFields: React.FC = ({ ); }; -export default ProviderSpecificFields; \ No newline at end of file +export default ProviderSpecificFields; diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index 0ca9e20b8e..1d42ac0974 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -17,6 +17,7 @@ export enum Providers { ElevenLabs = "ElevenLabs", FireworksAI = "Fireworks AI", Google_AI_Studio = "Google AI Studio", + GradientAI = "GradientAI", Groq = "Groq", JinaAI = "Jina AI", MistralAI = "Mistral AI", @@ -35,7 +36,7 @@ export enum Providers { Voyage = "Voyage AI", xAI = "xAI", } - + export const provider_map: Record = { OpenAI: "openai", OpenAI_Text: "text-completion-openai", @@ -61,6 +62,7 @@ export const provider_map: Record = { TogetherAI: "together_ai", Openrouter: "openrouter", FireworksAI: "fireworks_ai", + GradientAI: "gradient_ai", Triton: "triton", Deepgram: "deepgram", ElevenLabs: "elevenlabs", @@ -99,6 +101,7 @@ export const providerLogoMap: Record = { [Providers.TogetherAI]: `${asset_logos_folder}togetherai.svg`, [Providers.Vertex_AI]: `${asset_logos_folder}google.svg`, [Providers.xAI]: `${asset_logos_folder}xai.svg`, + [Providers.GradientAI]: `${asset_logos_folder}gradientai.svg`, [Providers.Triton]: `${asset_logos_folder}nvidia_triton.png`, [Providers.Deepgram]: `${asset_logos_folder}deepgram.png`, [Providers.ElevenLabs]: `${asset_logos_folder}elevenlabs.png`, @@ -169,9 +172,9 @@ export const getPlaceholder = (selectedProvider: string): string => { console.log(`Provider key: ${providerKey}`); let custom_llm_provider = provider_map[providerKey]; console.log(`Provider mapped to: ${custom_llm_provider}`); - + let providerModels: Array = []; - + if (providerKey && typeof modelMap === "object") { Object.entries(modelMap).forEach(([key, value]) => { if ( @@ -184,7 +187,6 @@ export const getPlaceholder = (selectedProvider: string): string => { providerModels.push(key); } }); - // Special case for cohere // we need both cohere_chat and cohere models to show on dropdown if (providerKey == Providers.Cohere) { @@ -217,6 +219,6 @@ export const getPlaceholder = (selectedProvider: string): string => { }); } } - + return providerModels; };