From 7a92a03565cd877dd2a48b76b89ee4a0fcee75ef Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 26 Mar 2025 00:03:46 +0800 Subject: [PATCH] Add new model provider Novita AI (#7582) * feat: add new model provider Novita AI * feat: use deepseek r1 model for examples in Novita AI docs * fix: fix tests * fix: fix tests for novita * fix: fix novita transformation --- .env.example | 4 +- README.md | 1 + cookbook/LiteLLM_NovitaAI_Cookbook.ipynb | 97 +++++++++++++++ cookbook/LiteLLM_OpenRouter.ipynb | 114 +++++++++--------- docs/my-website/docs/completion/input.md | 1 + docs/my-website/docs/index.md | 33 +++++ docs/my-website/docs/providers/novita.md | 39 ++++++ docs/my-website/sidebars.js | 1 + docs/my-website/src/pages/completion/input.md | 2 +- .../src/pages/completion/supported.md | 26 +++- docs/my-website/src/pages/index.md | 32 +++++ litellm/__init__.py | 8 +- litellm/constants.py | 2 + .../get_llm_provider_logic.py | 7 ++ .../get_supported_openai_params.py | 2 + litellm/llms/novita/chat/transformation.py | 35 ++++++ litellm/main.py | 1 - litellm/types/utils.py | 1 + litellm/utils.py | 15 +++ poetry.lock | 108 +++++++++++++---- .../llms/novita/chat/test_transformation.py | 67 ++++++++++ tests/local_testing/test_completion.py | 71 +++++++++++ 22 files changed, 581 insertions(+), 86 deletions(-) create mode 100644 cookbook/LiteLLM_NovitaAI_Cookbook.ipynb create mode 100644 docs/my-website/docs/providers/novita.md create mode 100644 litellm/llms/novita/chat/transformation.py create mode 100644 tests/litellm/llms/novita/chat/test_transformation.py diff --git a/.env.example b/.env.example index 82b09ca25e..ab615bc344 100644 --- a/.env.example +++ b/.env.example @@ -20,8 +20,10 @@ REPLICATE_API_TOKEN = "" ANTHROPIC_API_KEY = "" # Infisical INFISICAL_TOKEN = "" +# Novita AI +NOVITA_API_KEY = "" # Development Configs LITELLM_MASTER_KEY = "sk-1234" DATABASE_URL = "postgresql://llmproxy:dbpassword9090@db:5432/litellm" -STORE_MODEL_IN_DB = "True" \ No newline at end of file +STORE_MODEL_IN_DB = "True" diff --git a/README.md b/README.md index 2d2f71e4d1..61e69b883a 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,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) | ✅ | ✅ | ✅ | ✅ | | | +| [Novita AI](https://novita.ai/models/llm?utm_source=github_litellm&utm_medium=github_readme&utm_campaign=github_link) | ✅ | ✅ | ✅ | ✅ | | | [**Read the Docs**](https://docs.litellm.ai/docs/) diff --git a/cookbook/LiteLLM_NovitaAI_Cookbook.ipynb b/cookbook/LiteLLM_NovitaAI_Cookbook.ipynb new file mode 100644 index 0000000000..8fa7d0b987 --- /dev/null +++ b/cookbook/LiteLLM_NovitaAI_Cookbook.ipynb @@ -0,0 +1,97 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "iFEmsVJI_2BR" + }, + "source": [ + "# LiteLLM NovitaAI Cookbook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cBlUhCEP_xj4" + }, + "outputs": [], + "source": [ + "!pip install litellm" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "p-MQqWOT_1a7" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "os.environ['NOVITA_API_KEY'] = \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ze8JqMqWAARO" + }, + "outputs": [], + "source": [ + "from litellm import completion\n", + "response = completion(\n", + " model=\"novita/deepseek/deepseek-r1\",\n", + " messages=[{\"role\": \"user\", \"content\": \"write code for saying hi\"}]\n", + ")\n", + "response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-LnhELrnAM_J" + }, + "outputs": [], + "source": [ + "response = completion(\n", + " model=\"novita/deepseek/deepseek-r1\",\n", + " messages=[{\"role\": \"user\", \"content\": \"write code for saying hi\"}]\n", + ")\n", + "response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dJBOUYdwCEn1" + }, + "outputs": [], + "source": [ + "response = completion(\n", + " model=\"mistralai/mistral-7b-instruct\",\n", + " messages=[{\"role\": \"user\", \"content\": \"write code for saying hi\"}]\n", + ")\n", + "response" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/cookbook/LiteLLM_OpenRouter.ipynb b/cookbook/LiteLLM_OpenRouter.ipynb index e0d03e1258..6444b23b29 100644 --- a/cookbook/LiteLLM_OpenRouter.ipynb +++ b/cookbook/LiteLLM_OpenRouter.ipynb @@ -1,27 +1,13 @@ { - "nbformat": 4, - "nbformat_minor": 0, - "metadata": { - "colab": { - "provenance": [] - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - }, - "language_info": { - "name": "python" - } - }, "cells": [ { "cell_type": "markdown", - "source": [ - "# LiteLLM OpenRouter Cookbook" - ], "metadata": { "id": "iFEmsVJI_2BR" - } + }, + "source": [ + "# LiteLLM OpenRouter Cookbook" + ] }, { "cell_type": "code", @@ -36,27 +22,20 @@ }, { "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "p-MQqWOT_1a7" + }, + "outputs": [], "source": [ "import os\n", "\n", "os.environ['OPENROUTER_API_KEY'] = \"\"" - ], - "metadata": { - "id": "p-MQqWOT_1a7" - }, - "execution_count": 14, - "outputs": [] + ] }, { "cell_type": "code", - "source": [ - "from litellm import completion\n", - "response = completion(\n", - " model=\"openrouter/google/palm-2-chat-bison\",\n", - " messages=[{\"role\": \"user\", \"content\": \"write code for saying hi\"}]\n", - ")\n", - "response" - ], + "execution_count": 11, "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -64,10 +43,8 @@ "id": "Ze8JqMqWAARO", "outputId": "64f3e836-69fa-4f8e-fb35-088a913bbe98" }, - "execution_count": 11, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ " JSON: {\n", @@ -85,20 +62,23 @@ "}" ] }, + "execution_count": 11, "metadata": {}, - "execution_count": 11 + "output_type": "execute_result" } + ], + "source": [ + "from litellm import completion\n", + "response = completion(\n", + " model=\"openrouter/google/palm-2-chat-bison\",\n", + " messages=[{\"role\": \"user\", \"content\": \"write code for saying hi\"}]\n", + ")\n", + "response" ] }, { "cell_type": "code", - "source": [ - "response = completion(\n", - " model=\"openrouter/anthropic/claude-2\",\n", - " messages=[{\"role\": \"user\", \"content\": \"write code for saying hi\"}]\n", - ")\n", - "response" - ], + "execution_count": 12, "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -106,10 +86,8 @@ "id": "-LnhELrnAM_J", "outputId": "d51c7ab7-d761-4bd1-f849-1534d9df4cd0" }, - "execution_count": 12, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ " JSON: {\n", @@ -128,20 +106,22 @@ "}" ] }, + "execution_count": 12, "metadata": {}, - "execution_count": 12 + "output_type": "execute_result" } + ], + "source": [ + "response = completion(\n", + " model=\"openrouter/anthropic/claude-2\",\n", + " messages=[{\"role\": \"user\", \"content\": \"write code for saying hi\"}]\n", + ")\n", + "response" ] }, { "cell_type": "code", - "source": [ - "response = completion(\n", - " model=\"openrouter/meta-llama/llama-2-70b-chat\",\n", - " messages=[{\"role\": \"user\", \"content\": \"write code for saying hi\"}]\n", - ")\n", - "response" - ], + "execution_count": 13, "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -149,10 +129,8 @@ "id": "dJBOUYdwCEn1", "outputId": "ffa18679-ec15-4dad-fe2b-68665cdf36b0" }, - "execution_count": 13, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ " JSON: {\n", @@ -170,10 +148,32 @@ "}" ] }, + "execution_count": 13, "metadata": {}, - "execution_count": 13 + "output_type": "execute_result" } + ], + "source": [ + "response = completion(\n", + " model=\"openrouter/meta-llama/llama-2-70b-chat\",\n", + " messages=[{\"role\": \"user\", \"content\": \"write code for saying hi\"}]\n", + ")\n", + "response" ] } - ] -} \ No newline at end of file + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/docs/my-website/docs/completion/input.md b/docs/my-website/docs/completion/input.md index a8aa79b8cb..d4ed0d2997 100644 --- a/docs/my-website/docs/completion/input.md +++ b/docs/my-website/docs/completion/input.md @@ -62,6 +62,7 @@ Use `litellm.get_supported_openai_params()` for an updated list of params for ea |Databricks| ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | | | | | | | | | |ClarifAI| ✅ | ✅ | ✅ | |✅ | ✅ | | | | | | | | | | | |Github| ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | | | ✅ |✅ (model dependent)|✅ (model dependent)| | | +|Novita AI| ✅ | ✅ | | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ | | | ✅ | | | | | | | | :::note By default, LiteLLM raises an exception if the openai param being passed in isn't supported. diff --git a/docs/my-website/docs/index.md b/docs/my-website/docs/index.md index 9e4d76b89c..58cabc81b4 100644 --- a/docs/my-website/docs/index.md +++ b/docs/my-website/docs/index.md @@ -208,6 +208,22 @@ response = completion( ) ``` + + + +```python +from litellm import completion +import os + +## set ENV variables. Visit https://novita.ai/settings/key-management to get your API key +os.environ["NOVITA_API_KEY"] = "novita-api-key" + +response = completion( + model="novita/deepseek/deepseek-r1", + messages=[{ "content": "Hello, how are you?","role": "user"}] +) +``` + @@ -411,6 +427,23 @@ response = completion( ) ``` + + + +```python +from litellm import completion +import os + +## set ENV variables. Visit https://novita.ai/settings/key-management to get your API key +os.environ["NOVITA_API_KEY"] = "novita_api_key" + +response = completion( + model="novita/deepseek/deepseek-r1", + messages = [{ "content": "Hello, how are you?","role": "user"}], + stream=True, +) +``` + diff --git a/docs/my-website/docs/providers/novita.md b/docs/my-website/docs/providers/novita.md new file mode 100644 index 0000000000..88deddd141 --- /dev/null +++ b/docs/my-website/docs/providers/novita.md @@ -0,0 +1,39 @@ +# Novita AI +LiteLLM supports all models from [Novita AI](https://novita.ai/models/llm?utm_source=github_litellm&utm_medium=github_readme&utm_campaign=github_link) + +## Usage +```python +import os +from litellm import completion +os.environ["NOVITA_API_KEY"] = "" + +response = completion( + model="meta-llama/llama-3.3-70b-instruct", + messages=messages, + ) +``` + +## Novita AI Completion Models + +🚨 LiteLLM supports ALL Novita AI models, send `model=novita/` to send it to Novita AI. See all Novita AI models [here](https://novita.ai/models/llm?utm_source=github_litellm&utm_medium=github_readme&utm_campaign=github_link) + +| Model Name | Function Call | +|---------------------------|-----------------------------------------------------| +| novita/deepseek/deepseek-r1 | `completion('novita/deepseek/deepseek-r1', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/deepseek/deepseek_v3 | `completion('novita/deepseek/deepseek_v3', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.3-70b-instruct | `completion('novita/meta-llama/llama-3.3-70b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.1-8b-instruct | `completion('novita/meta-llama/llama-3.1-8b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.1-8b-instruct-max | `completion('novita/meta-llama/llama-3.1-8b-instruct-max', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.1-70b-instruct | `completion('novita/meta-llama/llama-3.1-70b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3-8b-instruct | `completion('novita/meta-llama/llama-3-8b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3-70b-instruct | `completion('novita/meta-llama/llama-3-70b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.2-1b-instruct | `completion('novita/meta-llama/llama-3.2-1b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.2-11b-vision-instruct | `completion('novita/meta-llama/llama-3.2-11b-vision-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.2-3b-instruct | `completion('novita/meta-llama/llama-3.2-3b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/gryphe/mythomax-l2-13b | `completion('novita/gryphe/mythomax-l2-13b', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/google/gemma-2-9b-it | `completion('novita/google/gemma-2-9b-it', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/mistralai/mistral-nemo | `completion('novita/mistralai/mistral-nemo', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/mistralai/mistral-7b-instruct | `completion('novita/mistralai/mistral-7b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/qwen/qwen-2.5-72b-instruct | `completion('novita/qwen/qwen-2.5-72b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/qwen/qwen-2-vl-72b-instruct | `completion('novita/qwen/qwen-2-vl-72b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | + diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 5aeee715d1..bdc48786c1 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -225,6 +225,7 @@ const sidebars = { "providers/nlp_cloud", "providers/replicate", "providers/togetherai", + "providers/novita", "providers/voyage", "providers/jina_ai", "providers/aleph_alpha", diff --git a/docs/my-website/src/pages/completion/input.md b/docs/my-website/src/pages/completion/input.md index 86546bbbae..ff9a3f0f0a 100644 --- a/docs/my-website/src/pages/completion/input.md +++ b/docs/my-website/src/pages/completion/input.md @@ -1,6 +1,6 @@ # Completion Function - completion() The Input params are **exactly the same** as the -OpenAI Create chat completion, and let you call **Azure OpenAI, Anthropic, Cohere, Replicate, OpenRouter** models in the same format. +OpenAI Create chat completion, and let you call **Azure OpenAI, Anthropic, Cohere, Replicate, OpenRouter, Novita AI** models in the same format. In addition, liteLLM allows you to pass in the following **Optional** liteLLM args: `force_timeout`, `azure`, `logger_fn`, `verbose` diff --git a/docs/my-website/src/pages/completion/supported.md b/docs/my-website/src/pages/completion/supported.md index 2599353aa3..097af2bb4c 100644 --- a/docs/my-website/src/pages/completion/supported.md +++ b/docs/my-website/src/pages/completion/supported.md @@ -70,4 +70,28 @@ All the text models from [OpenRouter](https://openrouter.ai/docs) are supported | google/palm-2-chat-bison | `completion('google/palm-2-chat-bison', messages)` | `os.environ['OR_SITE_URL']`,`os.environ['OR_APP_NAME']`,`os.environ['OR_API_KEY']` | | google/palm-2-codechat-bison | `completion('google/palm-2-codechat-bison', messages)` | `os.environ['OR_SITE_URL']`,`os.environ['OR_APP_NAME']`,`os.environ['OR_API_KEY']` | | meta-llama/llama-2-13b-chat | `completion('meta-llama/llama-2-13b-chat', messages)` | `os.environ['OR_SITE_URL']`,`os.environ['OR_APP_NAME']`,`os.environ['OR_API_KEY']` | -| meta-llama/llama-2-70b-chat | `completion('meta-llama/llama-2-70b-chat', messages)` | `os.environ['OR_SITE_URL']`,`os.environ['OR_APP_NAME']`,`os.environ['OR_API_KEY']` | \ No newline at end of file +| meta-llama/llama-2-70b-chat | `completion('meta-llama/llama-2-70b-chat', messages)` | `os.environ['OR_SITE_URL']`,`os.environ['OR_APP_NAME']`,`os.environ['OR_API_KEY']` | + +## Novita AI Completion Models + +🚨 LiteLLM supports ALL Novita AI models, send `model=novita/` to send it to Novita AI. See all Novita AI models [here](https://novita.ai/models/llm?utm_source=github_litellm&utm_medium=github_readme&utm_campaign=github_link) + +| Model Name | Function Call | Required OS Variables | +|------------------|--------------------------------------------|--------------------------------------| +| novita/deepseek/deepseek-r1 | `completion('novita/deepseek/deepseek-r1', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/deepseek/deepseek_v3 | `completion('novita/deepseek/deepseek_v3', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.3-70b-instruct | `completion('novita/meta-llama/llama-3.3-70b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.1-8b-instruct | `completion('novita/meta-llama/llama-3.1-8b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.1-8b-instruct-max | `completion('novita/meta-llama/llama-3.1-8b-instruct-max', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.1-70b-instruct | `completion('novita/meta-llama/llama-3.1-70b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3-8b-instruct | `completion('novita/meta-llama/llama-3-8b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3-70b-instruct | `completion('novita/meta-llama/llama-3-70b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.2-1b-instruct | `completion('novita/meta-llama/llama-3.2-1b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.2-11b-vision-instruct | `completion('novita/meta-llama/llama-3.2-11b-vision-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/meta-llama/llama-3.2-3b-instruct | `completion('novita/meta-llama/llama-3.2-3b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/gryphe/mythomax-l2-13b | `completion('novita/gryphe/mythomax-l2-13b', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/google/gemma-2-9b-it | `completion('novita/google/gemma-2-9b-it', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/mistralai/mistral-nemo | `completion('novita/mistralai/mistral-nemo', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/mistralai/mistral-7b-instruct | `completion('novita/mistralai/mistral-7b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/qwen/qwen-2.5-72b-instruct | `completion('novita/qwen/qwen-2.5-72b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | +| novita/qwen/qwen-2-vl-72b-instruct | `completion('novita/qwen/qwen-2-vl-72b-instruct', messages)` | `os.environ['NOVITA_API_KEY']` | \ No newline at end of file diff --git a/docs/my-website/src/pages/index.md b/docs/my-website/src/pages/index.md index 4a2e5203e3..2c89d28a62 100644 --- a/docs/my-website/src/pages/index.md +++ b/docs/my-website/src/pages/index.md @@ -194,6 +194,22 @@ response = completion( ) ``` + + + +```python +from litellm import completion +import os + +## set ENV variables. Visit https://novita.ai/settings/key-management to get your API key +os.environ["NOVITA_API_KEY"] = "novita-api-key" + +response = completion( + model="novita/deepseek/deepseek-r1", + messages=[{ "content": "Hello, how are you?","role": "user"}] +) +``` + @@ -347,7 +363,23 @@ response = completion( ``` + +```python +from litellm import completion +import os + +## set ENV variables. Visit https://novita.ai/settings/key-management to get your API key +os.environ["NOVITA_API_KEY"] = "novita_api_key" + +response = completion( + model="novita/deepseek/deepseek-r1", + messages = [{ "content": "Hello, how are you?","role": "user"}], + stream=True, +) +``` + + ### Exception handling diff --git a/litellm/__init__.py b/litellm/__init__.py index 4f0b0a16be..03eb70d9c9 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -182,6 +182,7 @@ cloudflare_api_key: Optional[str] = None baseten_key: Optional[str] = None aleph_alpha_key: Optional[str] = None nlp_cloud_key: Optional[str] = None +novita_api_key: Optional[str] = None snowflake_key: Optional[str] = None common_cloud_provider_auth_params: dict = { "params": ["project", "region_name", "token"], @@ -416,10 +417,10 @@ anyscale_models: List = [] cerebras_models: List = [] galadriel_models: List = [] sambanova_models: List = [] +novita_models: List = [] assemblyai_models: List = [] snowflake_models: List = [] - def is_bedrock_pricing_only_model(key: str) -> bool: """ Excludes keys with the pattern 'bedrock//'. These are in the model_prices_and_context_window.json file for pricing purposes only. @@ -567,6 +568,8 @@ def add_known_models(): galadriel_models.append(key) elif value.get("litellm_provider") == "sambanova_models": sambanova_models.append(key) + elif value.get("litellm_provider") == "novita": + novita_models.append(key) elif value.get("litellm_provider") == "assemblyai": assemblyai_models.append(key) elif value.get("litellm_provider") == "jina_ai": @@ -645,6 +648,7 @@ model_list = ( + galadriel_models + sambanova_models + azure_text_models + + novita_models + assemblyai_models + jina_ai_models + snowflake_models @@ -701,6 +705,7 @@ models_by_provider: dict = { "cerebras": cerebras_models, "galadriel": galadriel_models, "sambanova": sambanova_models, + "novita": novita_models, "assemblyai": assemblyai_models, "jina_ai": jina_ai_models, "snowflake": snowflake_models, @@ -835,6 +840,7 @@ from .llms.anthropic.experimental_pass_through.messages.transformation import ( from .llms.together_ai.chat import TogetherAIConfig from .llms.together_ai.completion.transformation import TogetherAITextCompletionConfig from .llms.cloudflare.chat.transformation import CloudflareChatConfig +from .llms.novita.chat.transformation import NovitaConfig from .llms.deprecated_providers.palm import ( PalmConfig, ) # here to prevent breaking changes diff --git a/litellm/constants.py b/litellm/constants.py index da66f897c9..64305efb86 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -82,6 +82,7 @@ LITELLM_CHAT_PROVIDERS = [ "hosted_vllm", "lm_studio", "galadriel", + "novita", ] @@ -171,6 +172,7 @@ openai_compatible_providers: List = [ "hosted_vllm", "lm_studio", "galadriel", + "novita", ] openai_text_completion_compatible_providers: List = ( [ # providers that support `/v1/completions` diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 037351d0e6..83b44571cb 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -569,6 +569,13 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915 or "https://api.galadriel.com/v1" ) # type: ignore dynamic_api_key = api_key or get_secret_str("GALADRIEL_API_KEY") + elif custom_llm_provider == "novita": + api_base = ( + api_base + or get_secret("NOVITA_API_BASE") + or "https://api.novita.ai/v3/openai" + ) # type: ignore + dynamic_api_key = api_key or get_secret_str("NOVITA_API_KEY") elif custom_llm_provider == "snowflake": api_base = ( api_base diff --git a/litellm/litellm_core_utils/get_supported_openai_params.py b/litellm/litellm_core_utils/get_supported_openai_params.py index 3d4f8cef6f..2301f23bba 100644 --- a/litellm/litellm_core_utils/get_supported_openai_params.py +++ b/litellm/litellm_core_utils/get_supported_openai_params.py @@ -119,6 +119,8 @@ def get_supported_openai_params( # noqa: PLR0915 return litellm.GoogleAIStudioGeminiConfig().get_supported_openai_params( model=model ) + elif custom_llm_provider == "novita": + return litellm.NovitaConfig().get_supported_openai_params(model=model) elif custom_llm_provider == "vertex_ai" or custom_llm_provider == "vertex_ai_beta": if request_type == "chat_completion": if model.startswith("mistral"): diff --git a/litellm/llms/novita/chat/transformation.py b/litellm/llms/novita/chat/transformation.py new file mode 100644 index 0000000000..bf30ba455b --- /dev/null +++ b/litellm/llms/novita/chat/transformation.py @@ -0,0 +1,35 @@ +""" +Support for OpenAI's `/v1/chat/completions` endpoint. + +Calls done in OpenAI/openai.py as Novita AI is openai-compatible. + +Docs: https://novita.ai/docs/guides/llm-api +""" + +from typing import List, Optional + +from ....types.llms.openai import AllMessageValues +from ...openai.chat.gpt_transformation import OpenAIGPTConfig + + +class NovitaConfig(OpenAIGPTConfig): + def validate_environment( + self, + headers: dict, + model: str, + messages: List[AllMessageValues], + optional_params: dict, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + ) -> dict: + if api_key is None: + raise ValueError( + "Missing Novita AI API Key - A call is being made to novita but no key is set either in the environment variables or via params" + ) + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "X-Novita-Source": "litellm", + } + return headers diff --git a/litellm/main.py b/litellm/main.py index 1826f2df78..6786b487ec 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -3252,7 +3252,6 @@ async def aembedding(*args, **kwargs) -> EmbeddingResponse: response = init_response elif asyncio.iscoroutine(init_response): response = await init_response # type: ignore - if ( response is not None and isinstance(response, EmbeddingResponse) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 2cc06eecbf..d0761d33a8 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2015,6 +2015,7 @@ class LlmProviders(str, Enum): GALADRIEL = "galadriel" INFINITY = "infinity" DEEPGRAM = "deepgram" + NOVITA = "novita" AIOHTTP_OPENAI = "aiohttp_openai" LANGFUSE = "langfuse" HUMANLOOP = "humanloop" diff --git a/litellm/utils.py b/litellm/utils.py index dc97c4d898..fc6c5d7d95 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -2317,6 +2317,9 @@ def register_model(model_cost: Union[str, dict]): # noqa: PLR0915 elif value.get("litellm_provider") == "bedrock": if key not in litellm.bedrock_models: litellm.bedrock_models.append(key) + elif value.get("litellm_provider") == "novita": + if key not in litellm.novita_models: + litellm.novita_models.append(key) return model_cost @@ -5073,6 +5076,11 @@ def validate_environment( # noqa: PLR0915 else: missing_keys.append("CLOUDFLARE_API_KEY") missing_keys.append("CLOUDFLARE_API_BASE") + elif custom_llm_provider == "novita": + if "NOVITA_API_KEY" in os.environ: + keys_in_environment = True + else: + missing_keys.append("NOVITA_API_KEY") else: ## openai - chatcompletion + text completion if ( @@ -5155,6 +5163,11 @@ def validate_environment( # noqa: PLR0915 keys_in_environment = True else: missing_keys.append("NLP_CLOUD_API_KEY") + elif model in litellm.novita_models: + if "NOVITA_API_KEY" in os.environ: + keys_in_environment = True + else: + missing_keys.append("NOVITA_API_KEY") if api_key is not None: new_missing_keys = [] @@ -6281,6 +6294,8 @@ class ProviderConfigManager: return litellm.TritonConfig() elif litellm.LlmProviders.PETALS == provider: return litellm.PetalsConfig() + elif litellm.LlmProviders.NOVITA == provider: + return litellm.NovitaConfig() elif litellm.LlmProviders.BEDROCK == provider: bedrock_route = BedrockModelInfo.get_bedrock_route(model) bedrock_invoke_provider = litellm.BedrockLLM.get_bedrock_invoke_provider( diff --git a/poetry.lock b/poetry.lock index fc2b4743bf..d741bb5720 100644 --- a/poetry.lock +++ b/poetry.lock @@ -123,7 +123,7 @@ multidict = ">=4.5,<7.0" yarl = ">=1.12.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] [[package]] name = "aiosignal" @@ -175,7 +175,7 @@ typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21.0b1) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -203,7 +203,7 @@ mongodb = ["pymongo (>=3.0)"] redis = ["redis (>=3.0)"] rethinkdb = ["rethinkdb (>=2.4.0)"] sqlalchemy = ["sqlalchemy (>=1.4)"] -test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6", "anyio (>=4.5.2)", "gevent", "pytest", "pytz", "twisted"] +test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6 ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "anyio (>=4.5.2)", "gevent ; python_version < \"3.14\"", "pytest", "pytz", "twisted ; python_version < \"3.14\""] tornado = ["tornado (>=4.3)"] twisted = ["twisted"] zookeeper = ["kazoo"] @@ -234,12 +234,12 @@ files = [ ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "azure-core" @@ -386,7 +386,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -865,7 +865,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] -typing = ["typing-extensions (>=4.12.2)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "flake8" @@ -1043,12 +1043,12 @@ files = [ google-auth = ">=2.14.1,<3.0.0" googleapis-common-protos = ">=1.56.2,<2.0.0" grpcio = [ + {version = ">=1.33.2,<2.0dev", optional = true, markers = "extra == \"grpc\""}, {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, - {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, ] grpcio-status = [ + {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""}, {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, ] proto-plus = [ {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, @@ -1059,7 +1059,7 @@ requests = ">=2.18.0,<3.0.0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] @@ -1352,6 +1352,66 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "h2" +version = "4.1.0" +description = "HTTP/2 State-Machine based protocol implementation" +optional = false +python-versions = ">=3.6.1" +groups = ["main"] +markers = "python_version < \"3.10\"" +files = [ + {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, + {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, +] + +[package.dependencies] +hpack = ">=4.0,<5" +hyperframe = ">=6.0,<7" + +[[package]] +name = "h2" +version = "4.2.0" +description = "Pure-Python HTTP/2 protocol implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, + {file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"}, +] + +[package.dependencies] +hpack = ">=4.1,<5" +hyperframe = ">=6.1,<7" + +[[package]] +name = "hpack" +version = "4.0.0" +description = "Pure-Python HPACK header compression" +optional = false +python-versions = ">=3.6.1" +groups = ["main"] +markers = "python_version < \"3.10\"" +files = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, +] + +[[package]] +name = "hpack" +version = "4.1.0" +description = "Pure-Python HPACK header encoding" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, + {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, +] + [[package]] name = "httpcore" version = "1.0.7" @@ -1393,7 +1453,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1481,12 +1541,12 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -1506,7 +1566,7 @@ files = [ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] @@ -1835,7 +1895,7 @@ PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} requests = ">=2.0.0,<3" [package.extras] -broker = ["pymsalruntime (>=0.14,<0.18)", "pymsalruntime (>=0.17,<0.18)"] +broker = ["pymsalruntime (>=0.14,<0.18) ; python_version >= \"3.6\" and platform_system == \"Windows\"", "pymsalruntime (>=0.17,<0.18) ; python_version >= \"3.8\" and platform_system == \"Darwin\""] [[package]] name = "msal-extensions" @@ -2614,7 +2674,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -3373,7 +3433,7 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] name = "six" version = "1.17.0" description = "Python 2 and 3 compatibility utilities" -optional = true +optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] markers = "extra == \"extra-proxy\" or extra == \"proxy\"" @@ -3688,7 +3748,7 @@ files = [ ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -3712,7 +3772,7 @@ h11 = ">=0.8" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "uvloop" @@ -3990,11 +4050,11 @@ files = [ ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [extras] diff --git a/tests/litellm/llms/novita/chat/test_transformation.py b/tests/litellm/llms/novita/chat/test_transformation.py new file mode 100644 index 0000000000..25b87e24dc --- /dev/null +++ b/tests/litellm/llms/novita/chat/test_transformation.py @@ -0,0 +1,67 @@ +""" +Unit tests for Novita AI configuration. + +These tests validate the NovitaConfig class which extends OpenAIGPTConfig. +Novita AI is an OpenAI-compatible provider with a few customizations. +""" + +import os +import sys +from typing import Dict, List, Optional +from unittest.mock import patch + +import pytest + +sys.path.insert( + 0, os.path.abspath("../../../../..") +) # Adds the parent directory to the system path + +from litellm.llms.novita.chat.transformation import NovitaConfig + + +class TestNovitaConfig: + """Test class for NovitaConfig functionality""" + + def test_validate_environment(self): + """Test that validate_environment adds correct headers""" + config = NovitaConfig() + headers = {} + api_key = "fake-novita-key" + + result = config.validate_environment( + headers=headers, + model="novita/meta-llama/llama-3.3-70b-instruct", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + api_key=api_key, + api_base="https://api.novita.ai/v3/openai" + ) + + # Verify headers + assert result["Authorization"] == f"Bearer {api_key}" + assert result["Content-Type"] == "application/json" + assert result["X-Novita-Source"] == "litellm" + + def test_missing_api_key(self): + """Test error handling when API key is missing""" + config = NovitaConfig() + + with pytest.raises(ValueError) as excinfo: + config.validate_environment( + headers={}, + model="novita/meta-llama/llama-3.3-70b-instruct", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + api_key=None, + api_base="https://api.novita.ai/v3/openai" + ) + + assert "Missing Novita AI API Key" in str(excinfo.value) + + def test_inheritance(self): + """Test proper inheritance from OpenAIGPTConfig""" + config = NovitaConfig() + + from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig + assert isinstance(config, OpenAIGPTConfig) + assert hasattr(config, "get_supported_openai_params") \ No newline at end of file diff --git a/tests/local_testing/test_completion.py b/tests/local_testing/test_completion.py index 59f5a38f08..2fbae89664 100644 --- a/tests/local_testing/test_completion.py +++ b/tests/local_testing/test_completion.py @@ -4658,6 +4658,77 @@ def test_humanloop_completion(monkeypatch): messages=[{"role": "user", "content": "Tell me a joke."}], ) +def test_completion_novita_ai(): + litellm.set_verbose = True + messages = [ + {"role": "system", "content": "You're a good bot"}, + { + "role": "user", + "content": "Hey", + }, + ] + + from openai import OpenAI + openai_client = OpenAI(api_key="fake-key") + + with patch.object( + openai_client.chat.completions, "create", new=MagicMock() + ) as mock_call: + try: + completion( + model="novita/meta-llama/llama-3.3-70b-instruct", + messages=messages, + client=openai_client, + api_base="https://api.novita.ai/v3/openai", + ) + + mock_call.assert_called_once() + + # Verify model is passed correctly + assert mock_call.call_args.kwargs["model"] == "meta-llama/llama-3.3-70b-instruct" + # Verify messages are passed correctly + assert mock_call.call_args.kwargs["messages"] == messages + + except Exception as e: + pytest.fail(f"Error occurred: {e}") + + +@pytest.mark.parametrize( + "api_key", ["my-bad-api-key"] +) +def test_completion_novita_ai_dynamic_params(api_key): + try: + litellm.set_verbose = True + messages = [ + {"role": "system", "content": "You're a good bot"}, + { + "role": "user", + "content": "Hey", + }, + ] + + from openai import OpenAI + openai_client = OpenAI(api_key="fake-key") + + with patch.object( + openai_client.chat.completions, "create", side_effect=Exception("Invalid API key") + ) as mock_call: + try: + completion( + model="novita/meta-llama/llama-3.3-70b-instruct", + messages=messages, + api_key=api_key, + client=openai_client, + api_base="https://api.novita.ai/v3/openai", + ) + pytest.fail(f"This call should have failed!") + except Exception as e: + # This should fail with the mocked exception + assert "Invalid API key" in str(e) + + mock_call.assert_called_once() + except Exception as e: + pytest.fail(f"Unexpected error: {e}") def test_deepseek_reasoning_content_completion(): try: