From 0db990fa469722f343bf6b71d418661ebb2e258a Mon Sep 17 00:00:00 2001 From: neosantara-xyz Date: Sat, 7 Feb 2026 09:45:03 +0000 Subject: [PATCH 1/8] feat(neosantara): add Neosantara integration as OpenAI-compatible provider --- docs/my-website/docs/providers/neosantara.md | 201 +++++++ docs/my-website/sidebars.js | 1 + litellm/__init__.py | 9 + litellm/constants.py | 10 + .../get_llm_provider_logic.py | 3 + litellm/llms/openai_like/providers.json | 25 +- litellm/main.py | 3 + litellm/types/utils.py | 1 + model_prices_and_context_window.json | 556 ++++++++++++++++++ provider_endpoints_support.json | 10 + tests/test_litellm/test_neosantara.py | 140 +++++ 11 files changed, 950 insertions(+), 9 deletions(-) create mode 100644 docs/my-website/docs/providers/neosantara.md create mode 100644 tests/test_litellm/test_neosantara.py diff --git a/docs/my-website/docs/providers/neosantara.md b/docs/my-website/docs/providers/neosantara.md new file mode 100644 index 000000000000..dcd6114d4bcb --- /dev/null +++ b/docs/my-website/docs/providers/neosantara.md @@ -0,0 +1,201 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Neosantara + +## Overview + +| Property | Details | +|-------|-------| +| Description | Neosantara is a unified LLM gateway designed for developers in Indonesia, providing a single OpenAI-compatible interface to multiple top-tier AI models (OpenAI, Anthropic, Gemini, etc.). | +| Provider Route on LiteLLM | `neosantara/` | +| Link to Provider Doc | [Neosantara Dashboard ↗](https://app.neosantara.xyz) | +| Base URL | `https://api.neosantara.xyz/v1` | +| Supported Operations | [`/chat/completions`](#sample-usage), [`/embeddings`](#embeddings) | + +
+ +## What is Neosantara? + +Neosantara is a unified gateway that lets developers: +- **Access Multiple LLM Providers**: Unified interface for OpenAI, Anthropic, Gemini, and more. +- **Optimized for Indonesia**: Designed specifically for the needs of developers in the region. +- **Unified Billing**: Pay-As-You-Go system with local payment support. +- **OpenAI Compatible**: Seamlessly drop into existing OpenAI-based workflows. + +## Required Variables + +```python showLineNumbers title="Environment Variables" +os.environ["NEOSANTARA_API_KEY"] = "your-neosantara-api-key" +``` + +Get your Neosantara API key from [app.neosantara.xyz](https://app.neosantara.xyz). + +## Usage - LiteLLM Python SDK + + + + +```python showLineNumbers title="Neosantara Non-streaming Completion" +import os +import litellm +from litellm import completion + +os.environ["NEOSANTARA_API_KEY"] = "your-neosantara-api-key" + +messages = [{"content": "What is the capital of Indonesia?", "role": "user"}] + +# Neosantara call +response = completion( + model="neosantara/claude-3-haiku", + messages=messages +) + +print(response) +``` + + + + +```python showLineNumbers title="Neosantara Streaming Completion" +import os +import litellm +from litellm import completion + +os.environ["NEOSANTARA_API_KEY"] = "your-neosantara-api-key" + +messages = [{"content": "Write a short poem about Jakarta", "role": "user"}] + +# Neosantara call with streaming +response = completion( + model="neosantara/claude-3-haiku", + messages=messages, + stream=True +) + +for chunk in response: + print(chunk) +``` + + + + +```python showLineNumbers title="Neosantara Embeddings" +import os +import litellm +from litellm import embedding + +os.environ["NEOSANTARA_API_KEY"] = "your-neosantara-api-key" + +# Neosantara call +response = embedding( + model="neosantara/nusa-embedding-0001", + input=["Hello, how are you?"] +) + +print(response) +``` + + + + +## Usage - LiteLLM Proxy Server + +### 1. Set Neosantara Models on `config.yaml` + +```yaml +model_list: + - model_name: neosantara-claude-3-haiku + litellm_params: + model: neosantara/claude-3-haiku + api_key: os.environ/NEOSANTARA_API_KEY +``` + +### 2. Start Proxy + +```bash +litellm --config config.yaml +``` + +### 3. Test it + + + + +```shell +curl --location 'http://0.0.0.0:4000/chat/completions' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer sk-1234' \ +--data ' { + "model": "neosantara-claude-3-haiku", + "messages": [ + { + "role": "user", + "content": "what llm are you" + } + ] + } +' +``` + + + +```python +import openai +client = openai.OpenAI( + api_key="anything", + base_url="http://0.0.0.0:4000" +) + +response = client.chat.completions.create( + model="neosantara-claude-3-haiku", + messages = [ + { + "role": "user", + "content": "this is a test request, write a short poem" + } + ] +) + +print(response) +``` + + + +## Supported Models + +We support a wide range of models optimized for the Indonesian context and high-performance tasks. + +| Model Name | Model ID (for LiteLLM) | Provider | Description | +|------------|------------------------|----------|-------------| +| **Nusantara Base** | `neosantara/nusantara-base` | Gemini | Flagship balanced model | +| **Archipelago 70B** | `neosantara/archipelago-70b` | Llama 3.3 | Cultural context awareness | +| **Garda Beta Mini** | `neosantara/garda-beta-mini` | Groq/Paxsenix | Fast & efficient Indonesian understanding | +| **Claude 3 Haiku** | `neosantara/claude-3-haiku` | Bedrock | Near-instant responsiveness | +| **Claude 3 Sonnet** | `neosantara/claude-3-sonnet` | Bedrock | Balance of intelligence and speed | +| **Sahabat AI Llama v4** | `neosantara/sahabat-ai-llama-v4` | SahabatAI | Fine-tuned for Sahabat AI ecosystem | +| **Nusa Embedding 0001**| `neosantara/nusa-embedding-0001` | Embedding | Optimized for Indonesian search | + +:::info +**Note:** You can use any model supported by Neosantara by adding the `neosantara/` prefix to the model name in your LiteLLM calls. +::: + +## Supported OpenAI Parameters + +Neosantara supports all standard OpenAI-compatible parameters: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `messages` | array | **Required**. Array of message objects with 'role' and 'content' | +| `model` | string | **Required**. Model ID (e.g., `claude-3-haiku`, `archipelago-70b`) | +| `stream` | boolean | Optional. Enable streaming responses | +| `temperature` | float | Optional. Sampling temperature | +| `top_p` | float | Optional. Nucleus sampling parameter | +| `max_tokens` | integer | Optional. Maximum tokens to generate | +| `tools` | array | Optional. List of available tools/functions | +| `tool_choice` | string/object | Optional. Control tool/function calling | + +## Additional Resources + +- [Neosantara Dashboard](https://app.neosantara.xyz) +- [API Documentation](https://docs.neosantara.xyz) \ No newline at end of file diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 033faeda7c9b..809f469eb5f3 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -790,6 +790,7 @@ const sidebars = { "providers/moonshot", "providers/morph", "providers/nebius", + "providers/neosantara", "providers/nlp_cloud", "providers/nano-gpt", "providers/novita", diff --git a/litellm/__init__.py b/litellm/__init__.py index f857e10eed33..39daa9fed39d 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -543,6 +543,7 @@ def identify(event_details): nscale_models: Set = set() nebius_models: Set = set() nebius_embedding_models: Set = set() +neosantara_embedding_models: Set = set() aiml_models: Set = set() deepgram_models: Set = set() elevenlabs_models: Set = set() @@ -571,6 +572,7 @@ def identify(event_details): aws_polly_models: Set = set() gigachat_models: Set = set() llamagate_models: Set = set() +neosantara_models: Set = set() def is_bedrock_pricing_only_model(key: str) -> bool: @@ -832,6 +834,11 @@ def add_known_models(): gigachat_models.add(key) elif value.get("litellm_provider") == "llamagate": llamagate_models.add(key) + elif value.get("litellm_provider") == "neosantara": + if value.get("mode") == "embedding": + neosantara_embedding_models.add(key) + else: + neosantara_models.add(key) add_known_models() @@ -1042,6 +1049,7 @@ def add_known_models(): "aws_polly": aws_polly_models, "gigachat": gigachat_models, "llamagate": llamagate_models, + "neosantara": neosantara_models | neosantara_embedding_models, } # mapping for those models which have larger equivalents @@ -1077,6 +1085,7 @@ def add_known_models(): | nebius_embedding_models | sambanova_embedding_models | ovhcloud_embedding_models + | neosantara_embedding_models ) ####### IMAGE GENERATION MODELS ################### diff --git a/litellm/constants.py b/litellm/constants.py index 444e78f8ed42..ef9c7347b357 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -455,6 +455,7 @@ "lemonade", "docker_model_runner", "amazon_nova", + "neosantara", ] LITELLM_EMBEDDING_PROVIDERS_SUPPORTING_INPUT_ARRAY_OF_TOKENS = [ @@ -462,6 +463,7 @@ "azure", "hosted_vllm", "nebius", + "neosantara", ] @@ -606,6 +608,7 @@ "https://ai-gateway.vercel.sh/v1", "https://api.inference.wandb.ai/v1", "https://api.clarifai.com/v2/ext/openai/v1", + "https://api.neosantara.xyz/v1", ] @@ -664,6 +667,7 @@ "clarifai", "docker_model_runner", "ragflow", + "neosantara", ] openai_text_completion_compatible_providers: List = ( [ # providers that support `/v1/completions` @@ -904,6 +908,12 @@ ] ) +neosantara_embedding_models: set = set( + [ + "nusa-embedding-0001", + ] +) + WANDB_MODELS: set = set( [ # openai models diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 718773a1b162..fe3a7f4f115a 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -304,6 +304,9 @@ def get_llm_provider( # noqa: PLR0915 elif endpoint == "https://api.inference.wandb.ai/v1": custom_llm_provider = "wandb" dynamic_api_key = get_secret_str("WANDB_API_KEY") + elif endpoint == "https://api.neosantara.xyz/v1": + custom_llm_provider = "neosantara" + dynamic_api_key = get_secret_str("NEOSANTARA_API_KEY") if api_base is not None and not isinstance(api_base, str): raise Exception( diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index b4f9cbe42de1..1f00f6d0b045 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -76,15 +76,22 @@ "base_url": "https://api.gmi-serving.com/v1", "api_key_env": "GMI_API_KEY" }, - "sarvam": { - "base_url": "https://api.sarvam.ai/v1", - "api_key_env": "SARVAM_API_KEY", - "base_class": "openai_gpt", - "param_mappings": { - "max_completion_tokens": "max_tokens" + "sarvam": { + "base_url": "https://api.sarvam.ai/v1", + "api_key_env": "SARVAM_API_KEY", + "base_class": "openai_gpt", + "param_mappings": { + "max_completion_tokens": "max_tokens" + }, + "headers": { + "api-subscription-key": "{api_key}" + } }, - "headers": { - "api-subscription-key": "{api_key}" + "neosantara": { + "base_url": "https://api.neosantara.xyz/v1", + "api_key_env": "NEOSANTARA_API_KEY", + "param_mappings": { + "max_completion_tokens": "max_tokens" + } } } -} diff --git a/litellm/main.py b/litellm/main.py index 6d6bef81c266..3f87b80c7961 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -4755,7 +4755,10 @@ def embedding( # noqa: PLR0915 or custom_llm_provider == "together_ai" or custom_llm_provider == "nvidia_nim" or custom_llm_provider == "litellm_proxy" + or custom_llm_provider == "neosantara" or (model in litellm.open_ai_embedding_models and custom_llm_provider is None) + or custom_llm_provider in litellm.openai_compatible_providers + or JSONProviderRegistry.exists(custom_llm_provider) ): api_base = ( api_base diff --git a/litellm/types/utils.py b/litellm/types/utils.py index e1f780ffcc38..d18c444cc182 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3084,6 +3084,7 @@ class LlmProviders(str, Enum): GRADIENT_AI = "gradient_ai" LLAMA = "meta_llama" NSCALE = "nscale" + NEOSANTARA = "neosantara" PG_VECTOR = "pg_vector" S3_VECTORS = "s3_vectors" HELICONE = "helicone" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 9fa68a516735..286999efb956 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -34899,5 +34899,561 @@ "mode": "chat", "output_cost_per_token": 0, "supports_reasoning": true + }, + "neosantara/nusantara-base": { + "max_input_tokens": 64000, + "max_output_tokens": 2048, + "input_cost_per_token": 0.00000001875, + "output_cost_per_token": 0.00000009375, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/archipelago-70b": { + "max_input_tokens": 24000, + "max_output_tokens": 2048, + "input_cost_per_token": 0.000000294375, + "output_cost_per_token": 0.0000022839375, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/garda-beta-mini": { + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "input_cost_per_token": 0.00000015625, + "output_cost_per_token": 0.0000006561875, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/sea-lion-v4-27b-it": { + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000000021875, + "output_cost_per_token": 0.000000035, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/claude-3-haiku": { + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.00000025, + "output_cost_per_token": 0.00000125, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/claude-3-sonnet": { + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000003, + "output_cost_per_token": 0.000015, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/gemma-3-27b-it": { + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "input_cost_per_token": 0.0000000125, + "output_cost_per_token": 0.00000009375, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/granite-3-8b-instruct": { + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "input_cost_per_token": 0.00000018, + "output_cost_per_token": 0.00000072, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/llama-3.3-70b-instruct": { + "max_input_tokens": 32000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.00000028125, + "output_cost_per_token": 0.000002, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/holo2-30b": { + "max_input_tokens": 32000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.00000009375, + "output_cost_per_token": 0.00000028125, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/hermes-2-pro-mistral-7b": { + "max_input_tokens": 24000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000000028125, + "output_cost_per_token": 0.000000075, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/gemma2-9b-it": { + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 0.0000000125, + "output_cost_per_token": 0.0000000625, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/qwen3-32b": { + "max_input_tokens": 131072, + "max_output_tokens": 40960, + "input_cost_per_token": 0.00000030175, + "output_cost_per_token": 0.0000006139375, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/llama-3.3-70b-turbo": { + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "input_cost_per_token": 0.00000001875, + "output_cost_per_token": 0.000000075, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/gpt-oss-20b": { + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "input_cost_per_token": 0.000000025, + "output_cost_per_token": 0.0000001, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/llama-3.2-11b": { + "max_input_tokens": 8192, + "max_output_tokens": 2048, + "input_cost_per_token": 0.00000000625, + "output_cost_per_token": 0.000000025, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/apriel-1.6-thinker": { + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "input_cost_per_token": 0.0000001875, + "output_cost_per_token": 0.0000005625, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/kimi-k2": { + "max_input_tokens": 131000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.000001040125, + "output_cost_per_token": 0.0000031205625, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/deepseek-chat-v3.1": { + "max_input_tokens": 164000, + "max_output_tokens": 4029, + "input_cost_per_token": 0.0000002083125, + "output_cost_per_token": 0.0000005208125, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/glm-4.6": { + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "input_cost_per_token": 0.000000625, + "output_cost_per_token": 0.000001875, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/gemini-3-pro-preview": { + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "input_cost_per_token": 0.0000025, + "output_cost_per_token": 0.0000075, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/gemini-3-flash-preview": { + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "input_cost_per_token": 0.0000001, + "output_cost_per_token": 0.0000004, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/claude-4.5-sonnet": { + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000003, + "output_cost_per_token": 0.000015, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/gemini-3-flash": { + "max_input_tokens": 1000000, + "max_output_tokens": 2400, + "input_cost_per_token": 0.0000005, + "output_cost_per_token": 0.0000003, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/grok-code-fast": { + "max_input_tokens": 256000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.0000002, + "output_cost_per_token": 0.0000015, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/grok-4.1-fast-non-reasoning": { + "max_input_tokens": 2000000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.0000003, + "output_cost_per_token": 0.00000072, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/nusa-embedding-0001": { + "max_input_tokens": 8192, + "input_cost_per_token": 0.0000001, + "output_cost_per_token": 0, + "litellm_provider": "neosantara", + "mode": "embedding" + }, + "neosantara/gemini-embedding-001": { + "max_input_tokens": 2048, + "input_cost_per_token": 0.0000001, + "output_cost_per_token": 0, + "litellm_provider": "neosantara", + "mode": "embedding" + }, + "neosantara/nusantara-base": { + "max_input_tokens": 64000, + "max_output_tokens": 2048, + "input_cost_per_token": 0.00000001875, + "output_cost_per_token": 0.00000009375, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/archipelago-70b": { + "max_input_tokens": 24000, + "max_output_tokens": 2048, + "input_cost_per_token": 0.000000294375, + "output_cost_per_token": 0.0000022839375, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/garda-beta-mini": { + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "input_cost_per_token": 0.00000015625, + "output_cost_per_token": 0.0000006561875, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/sea-lion-v4-27b-it": { + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000000021875, + "output_cost_per_token": 0.000000035, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/claude-3-haiku": { + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.00000025, + "output_cost_per_token": 0.00000125, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/claude-3-sonnet": { + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000003, + "output_cost_per_token": 0.000015, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/gemma-3-27b-it": { + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "input_cost_per_token": 0.0000000125, + "output_cost_per_token": 0.00000009375, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/granite-3-8b-instruct": { + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "input_cost_per_token": 0.00000018, + "output_cost_per_token": 0.00000072, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/llama-3.3-nemotron-super-49b-v1.5": { + "max_input_tokens": 132000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.000000285, + "output_cost_per_token": 0.0000019994375, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/llama-3.3-70b-instruct": { + "max_input_tokens": 32000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.00000028125, + "output_cost_per_token": 0.000002, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/holo2-30b": { + "max_input_tokens": 32000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.00000009375, + "output_cost_per_token": 0.00000028125, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/hermes-2-pro-mistral-7b": { + "max_input_tokens": 24000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000000028125, + "output_cost_per_token": 0.000000075, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/gemma2-9b-it": { + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 0.0000000125, + "output_cost_per_token": 0.0000000625, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/qwen3-32b": { + "max_input_tokens": 131072, + "max_output_tokens": 40960, + "input_cost_per_token": 0.00000030175, + "output_cost_per_token": 0.0000006139375, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/llama-3.3-70b-turbo": { + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "input_cost_per_token": 0.00000001875, + "output_cost_per_token": 0.000000075, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/gpt-oss-20b": { + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "input_cost_per_token": 0.000000025, + "output_cost_per_token": 0.0000001, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/llama-3.2-11b": { + "max_input_tokens": 8192, + "max_output_tokens": 2048, + "input_cost_per_token": 0.00000000625, + "output_cost_per_token": 0.000000025, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/apriel-1.6-thinker": { + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "input_cost_per_token": 0.0000001875, + "output_cost_per_token": 0.0000005625, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/kimi-k2": { + "max_input_tokens": 131000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.000001040125, + "output_cost_per_token": 0.0000031205625, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/deepseek-chat-v3.1": { + "max_input_tokens": 164000, + "max_output_tokens": 4029, + "input_cost_per_token": 0.0000002083125, + "output_cost_per_token": 0.0000005208125, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/glm-4.6": { + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "input_cost_per_token": 0.000000625, + "output_cost_per_token": 0.000001875, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/gemini-3-flash-preview": { + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "input_cost_per_token": 0.0000001, + "output_cost_per_token": 0.0000004, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/claude-4.5-sonnet": { + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000003, + "output_cost_per_token": 0.000015, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/gemini-3-flash": { + "max_input_tokens": 1000000, + "max_output_tokens": 2400, + "input_cost_per_token": 0.0000005, + "output_cost_per_token": 0.000003, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/nusa-embedding-0001": { + "max_input_tokens": 8192, + "input_cost_per_token": 0.0000001, + "output_cost_per_token": 0, + "litellm_provider": "neosantara", + "mode": "embedding" + }, + "neosantara/gemini-embedding-001": { + "max_input_tokens": 2048, + "input_cost_per_token": 0.0000001, + "output_cost_per_token": 0, + "litellm_provider": "neosantara", + "mode": "embedding" + }, + "neosantara/kimi-k2:latest": { + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000001040125, + "output_cost_per_token": 0.0000031205625, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/kimi-k2:search": { + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000001040125, + "output_cost_per_token": 0.0000031205625, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/kimi-k2:research": { + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000001040125, + "output_cost_per_token": 0.0000031205625, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/kimi-k1": { + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000001040125, + "output_cost_per_token": 0.0000031205625, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/kimi-k2:math": { + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000001040125, + "output_cost_per_token": 0.0000031205625, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/kimi-k2:silent": { + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000001040125, + "output_cost_per_token": 0.0000031205625, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/glm-4.6-plus": { + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "input_cost_per_token": 0.000000625, + "output_cost_per_token": 0.000001875, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/glm-4.6v-flash": { + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "input_cost_per_token": 0.000000015625, + "output_cost_per_token": 0.000000046875, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/glm-4.5-flash": { + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "input_cost_per_token": 0.000000015625, + "output_cost_per_token": 0.000000046875, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/gemini-3-pro-preview": { + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "input_cost_per_token": 0.0000025, + "output_cost_per_token": 0.0000075, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/glm-4.7": { + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "input_cost_per_token": 0.000000625, + "output_cost_per_token": 0.000001875, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/kimi-k2-thinking": { + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "input_cost_per_token": 0.00000125, + "output_cost_per_token": 0.00000375, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/grok-code-fast": { + "max_input_tokens": 256000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.0000002, + "output_cost_per_token": 0.0000015, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/grok-4.1-fast-non-reasoning": { + "max_input_tokens": 2000000, + "max_output_tokens": 4096, + "input_cost_per_token": 0.0000003, + "output_cost_per_token": 0.00000072, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/llama-guard-3-8b": { + "max_input_tokens": 131072, + "max_output_tokens": 256, + "input_cost_per_token": 0.00000048, + "output_cost_per_token": 0.00000003, + "litellm_provider": "neosantara", + "mode": "chat" + }, + "neosantara/sahabat-ai-llama-v4": { + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "input_cost_per_token": 0.000000009375, + "output_cost_per_token": 0.0000000375, + "litellm_provider": "neosantara", + "mode": "chat" } } \ No newline at end of file diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index fd17b5309e83..a631b1cbcf32 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -1467,6 +1467,16 @@ "interactions": true } }, + "neosantara": { + "display_name": "Neosantara (`neosantara`)", + "url": "https://docs.litellm.ai/docs/providers/neosantara", + "endpoints": { + "chat_completions": true, + "messages": true, + "responses": true, + "embeddings": true + } + }, "nlp_cloud": { "display_name": "NLP Cloud (`nlp_cloud`)", "url": "https://docs.litellm.ai/docs/providers/nlp_cloud", diff --git a/tests/test_litellm/test_neosantara.py b/tests/test_litellm/test_neosantara.py new file mode 100644 index 000000000000..6d3a31a45c7f --- /dev/null +++ b/tests/test_litellm/test_neosantara.py @@ -0,0 +1,140 @@ +import sys, os +import pytest +import litellm +import httpx + +def test_neosantara_provider_info(): + """ + Test that Neosantara is correctly recognized as a provider and maps to the correct base URL. + """ + model, provider, api_key, api_base = litellm.get_llm_provider("neosantara/claude-3-haiku") + + assert provider == "neosantara" + assert api_base == "https://api.neosantara.xyz/v1" + assert model == "claude-3-haiku" + +def test_neosantara_completion_formatting(): + """ + Test that a completion call to Neosantara formats the request correctly as an OpenAI-like call. + """ + litellm.set_verbose = True + os.environ["NEOSANTARA_API_KEY"] = "sk-1234" + + # We mock the actual call to avoid network requests + with pytest.MonkeyPatch().context() as m: + def mock_send(self, request, **kwargs): + return httpx.Response( + 200, + content='{"choices": [{"message": {"content": "Hello world"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion"}'.encode(), + request=request + ) + + m.setattr("httpx.Client.send", mock_send) + + response = litellm.completion( + model="neosantara/claude-3-haiku", + messages=[{"role": "user", "content": "hi"}], + api_key="sk-1234" + ) + + assert response.choices[0].message.content == "Hello world" + assert response._hidden_params["custom_llm_provider"] == "neosantara" + assert response._hidden_params["api_base"] == "https://api.neosantara.xyz/v1" + +def test_neosantara_embedding_formatting(): + """ + Test that an embedding call to Neosantara formats the request correctly as an OpenAI-like call. + """ + litellm.set_verbose = True + os.environ["NEOSANTARA_API_KEY"] = "sk-1234" + + # We mock the actual call to avoid network requests + with pytest.MonkeyPatch().context() as m: + def mock_send(self, request, **kwargs): + return httpx.Response( + 200, + content='{"data": [{"embedding": [0.1, 0.2, 0.3], "index": 0, "object": "embedding"}], "model": "nusa-embedding-0001", "object": "list", "usage": {"prompt_tokens": 10, "total_tokens": 10}}'.encode(), + request=request + ) + + m.setattr("httpx.Client.send", mock_send) + + response = litellm.embedding( + model="neosantara/nusa-embedding-0001", + input=["hi"], + api_key="sk-1234" + ) + + assert response.data[0]["embedding"] == [0.1, 0.2, 0.3] + assert response._hidden_params["custom_llm_provider"] == "neosantara" + assert response._hidden_params["api_base"] == "https://api.neosantara.xyz/v1" + +def test_neosantara_auto_detection_api_base(): + """ + Test that Neosantara is automatically detected when the Neosantara API base URL is provided. + """ + model, provider, api_key, api_base = litellm.get_llm_provider( + model="claude-3-haiku", + api_base="https://api.neosantara.xyz/v1" + ) + + assert provider == "neosantara" + assert api_base == "https://api.neosantara.xyz/v1" + assert model == "claude-3-haiku" + +def test_neosantara_responses_api_bridge(): + """ + Test that Neosantara works with litellm.responses() API bridge. + """ + os.environ["NEOSANTARA_API_KEY"] = "sk-1234" + + with pytest.MonkeyPatch().context() as m: + def mock_send(self, request, **kwargs): + return httpx.Response( + 200, + content='{"choices": [{"message": {"content": "Hello from responses API"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "id": "resp-123", "created": 123456789}'.encode(), + request=request + ) + + m.setattr("httpx.Client.send", mock_send) + + response = litellm.responses( + model="neosantara/claude-3-haiku", + input="hi", + api_key="sk-1234" + ) + + assert response.model == "neosantara/claude-3-haiku" + assert response.output[0].content[0].text == "Hello from responses API" + +@pytest.mark.asyncio +async def test_neosantara_anthropic_messages_bridge(): + """ + Test that Neosantara works with litellm.anthropic_messages() API bridge. + """ + os.environ["NEOSANTARA_API_KEY"] = "sk-1234" + + with pytest.MonkeyPatch().context() as m: + async def mock_async_send(self, request, **kwargs): + return httpx.Response( + 200, + content='{"choices": [{"message": {"content": "Hello from messages API"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "id": "msg-123", "created": 123456789}'.encode(), + request=request + ) + + m.setattr("httpx.AsyncClient.send", mock_async_send) + + response = await litellm.anthropic_messages( + model="neosantara/claude-3-haiku", + messages=[{"role": "user", "content": "hi"}], + api_key="sk-1234", + max_tokens=100 + ) + + # Check both dict and object access to be robust + if isinstance(response, dict): + assert response["content"][0].text == "Hello from messages API" + assert response["role"] == "assistant" + else: + assert response.content[0].text == "Hello from messages API" + assert response.role == "assistant" From c5a429624a8f4c4146757ca5d892b15070f165bf Mon Sep 17 00:00:00 2001 From: neosantara-xyz Date: Sat, 7 Feb 2026 10:20:43 +0000 Subject: [PATCH 2/8] fix(neosantara): de-duplicate model cost keys in pricing file --- model_prices_and_context_window.json | 402 +++++++-------------------- 1 file changed, 94 insertions(+), 308 deletions(-) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 286999efb956..991bee82b69a 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -29912,7 +29912,9 @@ "mode": "chat", "output_cost_per_token": 1e-06, "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", - "supported_regions": ["global"], + "supported_regions": [ + "global" + ], "supports_function_calling": true, "supports_tool_choice": true }, @@ -29925,7 +29927,9 @@ "mode": "chat", "output_cost_per_token": 4e-06, "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", - "supported_regions": ["global"], + "supported_regions": [ + "global" + ], "supports_function_calling": true, "supports_tool_choice": true }, @@ -29938,7 +29942,9 @@ "mode": "chat", "output_cost_per_token": 1.2e-06, "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", - "supported_regions": ["global"], + "supported_regions": [ + "global" + ], "supports_function_calling": true, "supports_tool_choice": true }, @@ -29951,7 +29957,9 @@ "mode": "chat", "output_cost_per_token": 1.2e-06, "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", - "supported_regions": ["global"], + "supported_regions": [ + "global" + ], "supports_function_calling": true, "supports_tool_choice": true }, @@ -34903,556 +34911,334 @@ "neosantara/nusantara-base": { "max_input_tokens": 64000, "max_output_tokens": 2048, - "input_cost_per_token": 0.00000001875, - "output_cost_per_token": 0.00000009375, + "input_cost_per_token": 1.875e-08, + "output_cost_per_token": 9.375e-08, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/archipelago-70b": { "max_input_tokens": 24000, "max_output_tokens": 2048, - "input_cost_per_token": 0.000000294375, - "output_cost_per_token": 0.0000022839375, + "input_cost_per_token": 2.94375e-07, + "output_cost_per_token": 2.2839375e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/garda-beta-mini": { "max_input_tokens": 131072, "max_output_tokens": 8192, - "input_cost_per_token": 0.00000015625, - "output_cost_per_token": 0.0000006561875, + "input_cost_per_token": 1.5625e-07, + "output_cost_per_token": 6.561875e-07, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/sea-lion-v4-27b-it": { "max_input_tokens": 128000, "max_output_tokens": 4096, - "input_cost_per_token": 0.000000021875, - "output_cost_per_token": 0.000000035, + "input_cost_per_token": 2.1875e-08, + "output_cost_per_token": 3.5e-08, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/claude-3-haiku": { "max_input_tokens": 200000, "max_output_tokens": 4096, - "input_cost_per_token": 0.00000025, - "output_cost_per_token": 0.00000125, + "input_cost_per_token": 2.5e-07, + "output_cost_per_token": 1.25e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/claude-3-sonnet": { "max_input_tokens": 200000, "max_output_tokens": 4096, - "input_cost_per_token": 0.000003, - "output_cost_per_token": 0.000015, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 1.5e-05, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/gemma-3-27b-it": { "max_input_tokens": 32768, "max_output_tokens": 4096, - "input_cost_per_token": 0.0000000125, - "output_cost_per_token": 0.00000009375, + "input_cost_per_token": 1.25e-08, + "output_cost_per_token": 9.375e-08, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/granite-3-8b-instruct": { "max_input_tokens": 128000, "max_output_tokens": 2048, - "input_cost_per_token": 0.00000018, - "output_cost_per_token": 0.00000072, + "input_cost_per_token": 1.8e-07, + "output_cost_per_token": 7.2e-07, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/llama-3.3-70b-instruct": { "max_input_tokens": 32000, "max_output_tokens": 4096, - "input_cost_per_token": 0.00000028125, - "output_cost_per_token": 0.000002, + "input_cost_per_token": 2.8125e-07, + "output_cost_per_token": 2e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/holo2-30b": { "max_input_tokens": 32000, "max_output_tokens": 4096, - "input_cost_per_token": 0.00000009375, - "output_cost_per_token": 0.00000028125, + "input_cost_per_token": 9.375e-08, + "output_cost_per_token": 2.8125e-07, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/hermes-2-pro-mistral-7b": { "max_input_tokens": 24000, "max_output_tokens": 4096, - "input_cost_per_token": 0.000000028125, - "output_cost_per_token": 0.000000075, + "input_cost_per_token": 2.8125e-08, + "output_cost_per_token": 7.5e-08, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/gemma2-9b-it": { "max_input_tokens": 8192, "max_output_tokens": 8192, - "input_cost_per_token": 0.0000000125, - "output_cost_per_token": 0.0000000625, + "input_cost_per_token": 1.25e-08, + "output_cost_per_token": 6.25e-08, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/qwen3-32b": { "max_input_tokens": 131072, "max_output_tokens": 40960, - "input_cost_per_token": 0.00000030175, - "output_cost_per_token": 0.0000006139375, + "input_cost_per_token": 3.0175e-07, + "output_cost_per_token": 6.139375e-07, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/llama-3.3-70b-turbo": { "max_input_tokens": 8192, "max_output_tokens": 4096, - "input_cost_per_token": 0.00000001875, - "output_cost_per_token": 0.000000075, + "input_cost_per_token": 1.875e-08, + "output_cost_per_token": 7.5e-08, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/gpt-oss-20b": { "max_input_tokens": 131072, "max_output_tokens": 8192, - "input_cost_per_token": 0.000000025, - "output_cost_per_token": 0.0000001, + "input_cost_per_token": 2.5e-08, + "output_cost_per_token": 1e-07, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/llama-3.2-11b": { "max_input_tokens": 8192, "max_output_tokens": 2048, - "input_cost_per_token": 0.00000000625, - "output_cost_per_token": 0.000000025, + "input_cost_per_token": 6.25e-09, + "output_cost_per_token": 2.5e-08, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/apriel-1.6-thinker": { "max_input_tokens": 32768, "max_output_tokens": 4096, - "input_cost_per_token": 0.0000001875, - "output_cost_per_token": 0.0000005625, + "input_cost_per_token": 1.875e-07, + "output_cost_per_token": 5.625e-07, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/kimi-k2": { "max_input_tokens": 131000, "max_output_tokens": 128000, - "input_cost_per_token": 0.000001040125, - "output_cost_per_token": 0.0000031205625, + "input_cost_per_token": 1.040125e-06, + "output_cost_per_token": 3.1205625e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/deepseek-chat-v3.1": { "max_input_tokens": 164000, "max_output_tokens": 4029, - "input_cost_per_token": 0.0000002083125, - "output_cost_per_token": 0.0000005208125, + "input_cost_per_token": 2.083125e-07, + "output_cost_per_token": 5.208125e-07, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/glm-4.6": { "max_input_tokens": 128000, "max_output_tokens": 8192, - "input_cost_per_token": 0.000000625, - "output_cost_per_token": 0.000001875, + "input_cost_per_token": 6.25e-07, + "output_cost_per_token": 1.875e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/gemini-3-pro-preview": { "max_input_tokens": 128000, "max_output_tokens": 8192, - "input_cost_per_token": 0.0000025, - "output_cost_per_token": 0.0000075, + "input_cost_per_token": 2.5e-06, + "output_cost_per_token": 7.5e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/gemini-3-flash-preview": { "max_input_tokens": 1000000, "max_output_tokens": 8192, - "input_cost_per_token": 0.0000001, - "output_cost_per_token": 0.0000004, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 4e-07, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/claude-4.5-sonnet": { "max_input_tokens": 200000, "max_output_tokens": 4096, - "input_cost_per_token": 0.000003, - "output_cost_per_token": 0.000015, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 1.5e-05, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/gemini-3-flash": { "max_input_tokens": 1000000, "max_output_tokens": 2400, - "input_cost_per_token": 0.0000005, - "output_cost_per_token": 0.0000003, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 3e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/grok-code-fast": { "max_input_tokens": 256000, "max_output_tokens": 4096, - "input_cost_per_token": 0.0000002, - "output_cost_per_token": 0.0000015, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 1.5e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/grok-4.1-fast-non-reasoning": { "max_input_tokens": 2000000, "max_output_tokens": 4096, - "input_cost_per_token": 0.0000003, - "output_cost_per_token": 0.00000072, + "input_cost_per_token": 3e-07, + "output_cost_per_token": 7.2e-07, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/nusa-embedding-0001": { "max_input_tokens": 8192, - "input_cost_per_token": 0.0000001, + "input_cost_per_token": 1e-07, "output_cost_per_token": 0, "litellm_provider": "neosantara", "mode": "embedding" }, "neosantara/gemini-embedding-001": { "max_input_tokens": 2048, - "input_cost_per_token": 0.0000001, + "input_cost_per_token": 1e-07, "output_cost_per_token": 0, "litellm_provider": "neosantara", "mode": "embedding" }, - "neosantara/nusantara-base": { - "max_input_tokens": 64000, - "max_output_tokens": 2048, - "input_cost_per_token": 0.00000001875, - "output_cost_per_token": 0.00000009375, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/archipelago-70b": { - "max_input_tokens": 24000, - "max_output_tokens": 2048, - "input_cost_per_token": 0.000000294375, - "output_cost_per_token": 0.0000022839375, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/garda-beta-mini": { - "max_input_tokens": 131072, - "max_output_tokens": 8192, - "input_cost_per_token": 0.00000015625, - "output_cost_per_token": 0.0000006561875, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/sea-lion-v4-27b-it": { - "max_input_tokens": 128000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.000000021875, - "output_cost_per_token": 0.000000035, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/claude-3-haiku": { - "max_input_tokens": 200000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.00000025, - "output_cost_per_token": 0.00000125, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/claude-3-sonnet": { - "max_input_tokens": 200000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.000003, - "output_cost_per_token": 0.000015, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/gemma-3-27b-it": { - "max_input_tokens": 32768, - "max_output_tokens": 4096, - "input_cost_per_token": 0.0000000125, - "output_cost_per_token": 0.00000009375, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/granite-3-8b-instruct": { - "max_input_tokens": 128000, - "max_output_tokens": 2048, - "input_cost_per_token": 0.00000018, - "output_cost_per_token": 0.00000072, - "litellm_provider": "neosantara", - "mode": "chat" - }, "neosantara/llama-3.3-nemotron-super-49b-v1.5": { "max_input_tokens": 132000, "max_output_tokens": 128000, - "input_cost_per_token": 0.000000285, - "output_cost_per_token": 0.0000019994375, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/llama-3.3-70b-instruct": { - "max_input_tokens": 32000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.00000028125, - "output_cost_per_token": 0.000002, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/holo2-30b": { - "max_input_tokens": 32000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.00000009375, - "output_cost_per_token": 0.00000028125, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/hermes-2-pro-mistral-7b": { - "max_input_tokens": 24000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.000000028125, - "output_cost_per_token": 0.000000075, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/gemma2-9b-it": { - "max_input_tokens": 8192, - "max_output_tokens": 8192, - "input_cost_per_token": 0.0000000125, - "output_cost_per_token": 0.0000000625, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/qwen3-32b": { - "max_input_tokens": 131072, - "max_output_tokens": 40960, - "input_cost_per_token": 0.00000030175, - "output_cost_per_token": 0.0000006139375, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/llama-3.3-70b-turbo": { - "max_input_tokens": 8192, - "max_output_tokens": 4096, - "input_cost_per_token": 0.00000001875, - "output_cost_per_token": 0.000000075, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/gpt-oss-20b": { - "max_input_tokens": 131072, - "max_output_tokens": 8192, - "input_cost_per_token": 0.000000025, - "output_cost_per_token": 0.0000001, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/llama-3.2-11b": { - "max_input_tokens": 8192, - "max_output_tokens": 2048, - "input_cost_per_token": 0.00000000625, - "output_cost_per_token": 0.000000025, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/apriel-1.6-thinker": { - "max_input_tokens": 32768, - "max_output_tokens": 4096, - "input_cost_per_token": 0.0000001875, - "output_cost_per_token": 0.0000005625, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/kimi-k2": { - "max_input_tokens": 131000, - "max_output_tokens": 128000, - "input_cost_per_token": 0.000001040125, - "output_cost_per_token": 0.0000031205625, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/deepseek-chat-v3.1": { - "max_input_tokens": 164000, - "max_output_tokens": 4029, - "input_cost_per_token": 0.0000002083125, - "output_cost_per_token": 0.0000005208125, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/glm-4.6": { - "max_input_tokens": 128000, - "max_output_tokens": 8192, - "input_cost_per_token": 0.000000625, - "output_cost_per_token": 0.000001875, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/gemini-3-flash-preview": { - "max_input_tokens": 1000000, - "max_output_tokens": 8192, - "input_cost_per_token": 0.0000001, - "output_cost_per_token": 0.0000004, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/claude-4.5-sonnet": { - "max_input_tokens": 200000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.000003, - "output_cost_per_token": 0.000015, + "input_cost_per_token": 2.85e-07, + "output_cost_per_token": 1.9994375e-06, "litellm_provider": "neosantara", "mode": "chat" }, - "neosantara/gemini-3-flash": { - "max_input_tokens": 1000000, - "max_output_tokens": 2400, - "input_cost_per_token": 0.0000005, - "output_cost_per_token": 0.000003, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/nusa-embedding-0001": { - "max_input_tokens": 8192, - "input_cost_per_token": 0.0000001, - "output_cost_per_token": 0, - "litellm_provider": "neosantara", - "mode": "embedding" - }, - "neosantara/gemini-embedding-001": { - "max_input_tokens": 2048, - "input_cost_per_token": 0.0000001, - "output_cost_per_token": 0, - "litellm_provider": "neosantara", - "mode": "embedding" - }, "neosantara/kimi-k2:latest": { "max_input_tokens": 128000, "max_output_tokens": 4096, - "input_cost_per_token": 0.000001040125, - "output_cost_per_token": 0.0000031205625, + "input_cost_per_token": 1.040125e-06, + "output_cost_per_token": 3.1205625e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/kimi-k2:search": { "max_input_tokens": 128000, "max_output_tokens": 4096, - "input_cost_per_token": 0.000001040125, - "output_cost_per_token": 0.0000031205625, + "input_cost_per_token": 1.040125e-06, + "output_cost_per_token": 3.1205625e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/kimi-k2:research": { "max_input_tokens": 128000, "max_output_tokens": 4096, - "input_cost_per_token": 0.000001040125, - "output_cost_per_token": 0.0000031205625, + "input_cost_per_token": 1.040125e-06, + "output_cost_per_token": 3.1205625e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/kimi-k1": { "max_input_tokens": 128000, "max_output_tokens": 4096, - "input_cost_per_token": 0.000001040125, - "output_cost_per_token": 0.0000031205625, + "input_cost_per_token": 1.040125e-06, + "output_cost_per_token": 3.1205625e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/kimi-k2:math": { "max_input_tokens": 128000, "max_output_tokens": 4096, - "input_cost_per_token": 0.000001040125, - "output_cost_per_token": 0.0000031205625, + "input_cost_per_token": 1.040125e-06, + "output_cost_per_token": 3.1205625e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/kimi-k2:silent": { "max_input_tokens": 128000, "max_output_tokens": 4096, - "input_cost_per_token": 0.000001040125, - "output_cost_per_token": 0.0000031205625, + "input_cost_per_token": 1.040125e-06, + "output_cost_per_token": 3.1205625e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/glm-4.6-plus": { "max_input_tokens": 128000, "max_output_tokens": 8192, - "input_cost_per_token": 0.000000625, - "output_cost_per_token": 0.000001875, + "input_cost_per_token": 6.25e-07, + "output_cost_per_token": 1.875e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/glm-4.6v-flash": { "max_input_tokens": 128000, "max_output_tokens": 8192, - "input_cost_per_token": 0.000000015625, - "output_cost_per_token": 0.000000046875, + "input_cost_per_token": 1.5625e-08, + "output_cost_per_token": 4.6875e-08, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/glm-4.5-flash": { "max_input_tokens": 128000, "max_output_tokens": 8192, - "input_cost_per_token": 0.000000015625, - "output_cost_per_token": 0.000000046875, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/gemini-3-pro-preview": { - "max_input_tokens": 128000, - "max_output_tokens": 8192, - "input_cost_per_token": 0.0000025, - "output_cost_per_token": 0.0000075, + "input_cost_per_token": 1.5625e-08, + "output_cost_per_token": 4.6875e-08, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/glm-4.7": { "max_input_tokens": 128000, "max_output_tokens": 8192, - "input_cost_per_token": 0.000000625, - "output_cost_per_token": 0.000001875, + "input_cost_per_token": 6.25e-07, + "output_cost_per_token": 1.875e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/kimi-k2-thinking": { "max_input_tokens": 128000, "max_output_tokens": 8192, - "input_cost_per_token": 0.00000125, - "output_cost_per_token": 0.00000375, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/grok-code-fast": { - "max_input_tokens": 256000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.0000002, - "output_cost_per_token": 0.0000015, - "litellm_provider": "neosantara", - "mode": "chat" - }, - "neosantara/grok-4.1-fast-non-reasoning": { - "max_input_tokens": 2000000, - "max_output_tokens": 4096, - "input_cost_per_token": 0.0000003, - "output_cost_per_token": 0.00000072, + "input_cost_per_token": 1.25e-06, + "output_cost_per_token": 3.75e-06, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/llama-guard-3-8b": { "max_input_tokens": 131072, "max_output_tokens": 256, - "input_cost_per_token": 0.00000048, - "output_cost_per_token": 0.00000003, + "input_cost_per_token": 4.8e-07, + "output_cost_per_token": 3e-08, "litellm_provider": "neosantara", "mode": "chat" }, "neosantara/sahabat-ai-llama-v4": { "max_input_tokens": 32768, "max_output_tokens": 4096, - "input_cost_per_token": 0.000000009375, - "output_cost_per_token": 0.0000000375, + "input_cost_per_token": 9.375e-09, + "output_cost_per_token": 3.75e-08, "litellm_provider": "neosantara", "mode": "chat" } From b80cf8624c118c1aef371bdfd9123e60730909dd Mon Sep 17 00:00:00 2001 From: neosantara-xyz Date: Sat, 7 Feb 2026 10:23:45 +0000 Subject: [PATCH 3/8] fix(neosantara): add missing endpoint flag keys in provider support metadata --- provider_endpoints_support.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index a631b1cbcf32..a94463ce653a 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -1474,7 +1474,20 @@ "chat_completions": true, "messages": true, "responses": true, - "embeddings": true + "embeddings": true, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": true, + "interactions": true, + "ocr": false, + "search": false, + "video_generations": false, + "compact": false, + "files": false } }, "nlp_cloud": { From b68ba9a81f11910e4caea6848181c638ba47c8b1 Mon Sep 17 00:00:00 2001 From: neosantara-xyz Date: Sat, 7 Feb 2026 10:28:07 +0000 Subject: [PATCH 4/8] fix(neosantara): fix bridge tests schema mocking and verification --- tests/test_litellm/test_neosantara.py | 46 ++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/tests/test_litellm/test_neosantara.py b/tests/test_litellm/test_neosantara.py index 6d3a31a45c7f..523baa5b1583 100644 --- a/tests/test_litellm/test_neosantara.py +++ b/tests/test_litellm/test_neosantara.py @@ -2,6 +2,7 @@ import pytest import litellm import httpx +import json def test_neosantara_provider_info(): """ @@ -23,6 +24,15 @@ def test_neosantara_completion_formatting(): # We mock the actual call to avoid network requests with pytest.MonkeyPatch().context() as m: def mock_send(self, request, **kwargs): + # Verify request URL and headers + assert "api.neosantara.xyz" in str(request.url) + assert request.headers["Authorization"] == "Bearer sk-1234" + + # Verify request body is OpenAI format + body = json.loads(request.read()) + assert "messages" in body + assert body["model"] == "claude-3-haiku" + return httpx.Response( 200, content='{"choices": [{"message": {"content": "Hello world"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion"}'.encode(), @@ -51,6 +61,10 @@ def test_neosantara_embedding_formatting(): # We mock the actual call to avoid network requests with pytest.MonkeyPatch().context() as m: def mock_send(self, request, **kwargs): + # Verify request URL + assert "api.neosantara.xyz" in str(request.url) + assert "/v1/embeddings" in str(request.url) + return httpx.Response( 200, content='{"data": [{"embedding": [0.1, 0.2, 0.3], "index": 0, "object": "embedding"}], "model": "nusa-embedding-0001", "object": "list", "usage": {"prompt_tokens": 10, "total_tokens": 10}}'.encode(), @@ -90,9 +104,15 @@ def test_neosantara_responses_api_bridge(): with pytest.MonkeyPatch().context() as m: def mock_send(self, request, **kwargs): + # Verify the bridge transformed 'input' into OpenAI 'messages' + body = json.loads(request.read()) + assert "messages" in body + assert body["messages"][0]["content"] == "hi from responses" + assert "/v1/chat/completions" in str(request.url) + return httpx.Response( 200, - content='{"choices": [{"message": {"content": "Hello from responses API"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "id": "resp-123", "created": 123456789}'.encode(), + content='{"choices": [{"message": {"content": "Hello from bridge"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "id": "chatcmpl-123", "created": 1677610602}'.encode(), request=request ) @@ -100,12 +120,14 @@ def mock_send(self, request, **kwargs): response = litellm.responses( model="neosantara/claude-3-haiku", - input="hi", + input="hi from responses", api_key="sk-1234" ) + # Verify result is transformed to Responses API schema assert response.model == "neosantara/claude-3-haiku" - assert response.output[0].content[0].text == "Hello from responses API" + assert response.output[0].content[0].text == "Hello from bridge" + assert hasattr(response, "created_at") @pytest.mark.asyncio async def test_neosantara_anthropic_messages_bridge(): @@ -116,9 +138,15 @@ async def test_neosantara_anthropic_messages_bridge(): with pytest.MonkeyPatch().context() as m: async def mock_async_send(self, request, **kwargs): + # Verify the bridge transformed Anthropic messages to OpenAI messages + body = json.loads(request.read()) + assert "messages" in body + assert body["messages"][0]["role"] == "user" + assert "/v1/chat/completions" in str(request.url) + return httpx.Response( 200, - content='{"choices": [{"message": {"content": "Hello from messages API"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "id": "msg-123", "created": 123456789}'.encode(), + content='{"choices": [{"message": {"content": "Hello from anthropic bridge"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "id": "chatcmpl-123", "created": 1677610602}'.encode(), request=request ) @@ -126,15 +154,17 @@ async def mock_async_send(self, request, **kwargs): response = await litellm.anthropic_messages( model="neosantara/claude-3-haiku", - messages=[{"role": "user", "content": "hi"}], + messages=[{"role": "user", "content": "hi from anthropic"}], api_key="sk-1234", max_tokens=100 ) - # Check both dict and object access to be robust + # Check both dict and object access to be robust for Anthropic bridge if isinstance(response, dict): - assert response["content"][0].text == "Hello from messages API" assert response["role"] == "assistant" + assert response["content"][0].text == "Hello from anthropic bridge" + assert response["type"] == "message" else: - assert response.content[0].text == "Hello from messages API" assert response.role == "assistant" + assert response.content[0].text == "Hello from anthropic bridge" + assert response.type == "message" From e26c2038cd9fce510e389074a709020adff4dc8f Mon Sep 17 00:00:00 2001 From: neosantara-xyz Date: Sat, 7 Feb 2026 10:32:21 +0000 Subject: [PATCH 5/8] fix(neosantara): fix providers.json structure and formatting --- litellm/llms/openai_like/providers.json | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index 1f00f6d0b045..6ce14919674e 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -76,22 +76,22 @@ "base_url": "https://api.gmi-serving.com/v1", "api_key_env": "GMI_API_KEY" }, - "sarvam": { - "base_url": "https://api.sarvam.ai/v1", - "api_key_env": "SARVAM_API_KEY", - "base_class": "openai_gpt", - "param_mappings": { - "max_completion_tokens": "max_tokens" - }, - "headers": { - "api-subscription-key": "{api_key}" - } + "sarvam": { + "base_url": "https://api.sarvam.ai/v1", + "api_key_env": "SARVAM_API_KEY", + "base_class": "openai_gpt", + "param_mappings": { + "max_completion_tokens": "max_tokens" }, - "neosantara": { - "base_url": "https://api.neosantara.xyz/v1", - "api_key_env": "NEOSANTARA_API_KEY", - "param_mappings": { - "max_completion_tokens": "max_tokens" - } + "headers": { + "api-subscription-key": "{api_key}" + } + }, + "neosantara": { + "base_url": "https://api.neosantara.xyz/v1", + "api_key_env": "NEOSANTARA_API_KEY", + "param_mappings": { + "max_completion_tokens": "max_tokens" } } +} \ No newline at end of file From 323fec146a438820468180930e9316112e4c6ed3 Mon Sep 17 00:00:00 2001 From: neosantara-xyz Date: Sat, 7 Feb 2026 15:30:50 +0000 Subject: [PATCH 6/8] fix(neosantara): improve auto-detection and robust test mocking --- litellm/constants.py | 2 +- .../get_llm_provider_logic.py | 2 +- tests/test_litellm/test_neosantara.py | 220 ++++++++++-------- 3 files changed, 125 insertions(+), 99 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index ef9c7347b357..ae695fb89357 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -608,7 +608,7 @@ "https://ai-gateway.vercel.sh/v1", "https://api.inference.wandb.ai/v1", "https://api.clarifai.com/v2/ext/openai/v1", - "https://api.neosantara.xyz/v1", + "api.neosantara.xyz/v1", ] diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index fe3a7f4f115a..8fe27755585f 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -304,7 +304,7 @@ def get_llm_provider( # noqa: PLR0915 elif endpoint == "https://api.inference.wandb.ai/v1": custom_llm_provider = "wandb" dynamic_api_key = get_secret_str("WANDB_API_KEY") - elif endpoint == "https://api.neosantara.xyz/v1": + elif endpoint == "api.neosantara.xyz/v1": custom_llm_provider = "neosantara" dynamic_api_key = get_secret_str("NEOSANTARA_API_KEY") diff --git a/tests/test_litellm/test_neosantara.py b/tests/test_litellm/test_neosantara.py index 523baa5b1583..f800f0f8e468 100644 --- a/tests/test_litellm/test_neosantara.py +++ b/tests/test_litellm/test_neosantara.py @@ -3,6 +3,7 @@ import litellm import httpx import json +from unittest.mock import MagicMock, patch def test_neosantara_provider_info(): """ @@ -14,130 +15,141 @@ def test_neosantara_provider_info(): assert api_base == "https://api.neosantara.xyz/v1" assert model == "claude-3-haiku" -def test_neosantara_completion_formatting(): +def test_neosantara_auto_detection_api_base(): + """ + Test that Neosantara is automatically detected when the Neosantara API base URL is provided. + """ + model, provider, api_key, api_base = litellm.get_llm_provider( + model="claude-3-haiku", + api_base="https://api.neosantara.xyz/v1" + ) + + assert provider == "neosantara" + assert api_base == "https://api.neosantara.xyz/v1" + assert model == "claude-3-haiku" + +def test_neosantara_auto_detection_api_base_no_protocol(): + """ + Test that Neosantara is automatically detected when the Neosantara API base URL is provided without protocol. + """ + model, provider, api_key, api_base = litellm.get_llm_provider( + model="claude-3-haiku", + api_base="api.neosantara.xyz/v1" + ) + + assert provider == "neosantara" + assert api_base == "api.neosantara.xyz/v1" + assert model == "claude-3-haiku" + +@patch("httpx.Client.send") +def test_neosantara_completion_formatting(mock_send): """ Test that a completion call to Neosantara formats the request correctly as an OpenAI-like call. """ - litellm.set_verbose = True os.environ["NEOSANTARA_API_KEY"] = "sk-1234" - # We mock the actual call to avoid network requests - with pytest.MonkeyPatch().context() as m: - def mock_send(self, request, **kwargs): - # Verify request URL and headers - assert "api.neosantara.xyz" in str(request.url) - assert request.headers["Authorization"] == "Bearer sk-1234" - - # Verify request body is OpenAI format - body = json.loads(request.read()) - assert "messages" in body - assert body["model"] == "claude-3-haiku" - - return httpx.Response( - 200, - content='{"choices": [{"message": {"content": "Hello world"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion"}'.encode(), - request=request - ) + def side_effect(request, **kwargs): + # Verify request URL and headers + assert "api.neosantara.xyz" in str(request.url) + assert request.headers["Authorization"] == "Bearer sk-1234" + + # Verify request body is OpenAI format + body = json.loads(request.read()) + assert "messages" in body + assert body["model"] == "claude-3-haiku" + + return httpx.Response( + 200, + content='{"id": "chatcmpl-123", "choices": [{"message": {"content": "Hello world", "role": "assistant"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "created": 1677610602, "model": "claude-3-haiku"}'.encode(), + request=request + ) - m.setattr("httpx.Client.send", mock_send) + mock_send.side_effect = side_effect - response = litellm.completion( - model="neosantara/claude-3-haiku", - messages=[{"role": "user", "content": "hi"}], - api_key="sk-1234" - ) - - assert response.choices[0].message.content == "Hello world" - assert response._hidden_params["custom_llm_provider"] == "neosantara" - assert response._hidden_params["api_base"] == "https://api.neosantara.xyz/v1" + response = litellm.completion( + model="neosantara/claude-3-haiku", + messages=[{"role": "user", "content": "hi"}], + api_key="sk-1234" + ) + + assert response.choices[0].message.content == "Hello world" + assert response._hidden_params["custom_llm_provider"] == "neosantara" + assert response._hidden_params["api_base"] == "https://api.neosantara.xyz/v1" -def test_neosantara_embedding_formatting(): +@patch("httpx.Client.send") +def test_neosantara_embedding_formatting(mock_send): """ Test that an embedding call to Neosantara formats the request correctly as an OpenAI-like call. """ - litellm.set_verbose = True os.environ["NEOSANTARA_API_KEY"] = "sk-1234" - # We mock the actual call to avoid network requests - with pytest.MonkeyPatch().context() as m: - def mock_send(self, request, **kwargs): - # Verify request URL - assert "api.neosantara.xyz" in str(request.url) - assert "/v1/embeddings" in str(request.url) - - return httpx.Response( - 200, - content='{"data": [{"embedding": [0.1, 0.2, 0.3], "index": 0, "object": "embedding"}], "model": "nusa-embedding-0001", "object": "list", "usage": {"prompt_tokens": 10, "total_tokens": 10}}'.encode(), - request=request - ) - - m.setattr("httpx.Client.send", mock_send) - - response = litellm.embedding( - model="neosantara/nusa-embedding-0001", - input=["hi"], - api_key="sk-1234" - ) + def side_effect(request, **kwargs): + # Verify request URL + assert "api.neosantara.xyz" in str(request.url) + assert "/v1/embeddings" in str(request.url) - assert response.data[0]["embedding"] == [0.1, 0.2, 0.3] - assert response._hidden_params["custom_llm_provider"] == "neosantara" - assert response._hidden_params["api_base"] == "https://api.neosantara.xyz/v1" + return httpx.Response( + 200, + content='{"data": [{"embedding": [0.1, 0.2, 0.3], "index": 0, "object": "embedding"}], "model": "nusa-embedding-0001", "object": "list", "usage": {"prompt_tokens": 10, "total_tokens": 10}}'.encode(), + request=request + ) -def test_neosantara_auto_detection_api_base(): - """ - Test that Neosantara is automatically detected when the Neosantara API base URL is provided. - """ - model, provider, api_key, api_base = litellm.get_llm_provider( - model="claude-3-haiku", - api_base="https://api.neosantara.xyz/v1" + mock_send.side_effect = side_effect + + response = litellm.embedding( + model="neosantara/nusa-embedding-0001", + input=["hi"], + api_key="sk-1234" ) - assert provider == "neosantara" - assert api_base == "https://api.neosantara.xyz/v1" - assert model == "claude-3-haiku" + assert response.data[0]["embedding"] == [0.1, 0.2, 0.3] + assert response._hidden_params["custom_llm_provider"] == "neosantara" + assert response._hidden_params["api_base"] == "https://api.neosantara.xyz/v1" -def test_neosantara_responses_api_bridge(): +@patch("httpx.Client.send") +def test_neosantara_responses_api_bridge(mock_send): """ Test that Neosantara works with litellm.responses() API bridge. """ os.environ["NEOSANTARA_API_KEY"] = "sk-1234" - with pytest.MonkeyPatch().context() as m: - def mock_send(self, request, **kwargs): - # Verify the bridge transformed 'input' into OpenAI 'messages' - body = json.loads(request.read()) - assert "messages" in body - assert body["messages"][0]["content"] == "hi from responses" - assert "/v1/chat/completions" in str(request.url) - - return httpx.Response( - 200, - content='{"choices": [{"message": {"content": "Hello from bridge"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "id": "chatcmpl-123", "created": 1677610602}'.encode(), - request=request - ) - - m.setattr("httpx.Client.send", mock_send) - - response = litellm.responses( - model="neosantara/claude-3-haiku", - input="hi from responses", - api_key="sk-1234" + def side_effect(request, **kwargs): + # Verify the bridge transformed 'input' into OpenAI 'messages' + body = json.loads(request.read()) + assert "messages" in body + assert body["messages"][0]["content"] == "hi from responses" + assert "/v1/chat/completions" in str(request.url) + + return httpx.Response( + 200, + content='{"id": "chatcmpl-123", "choices": [{"message": {"content": "Hello from bridge", "role": "assistant"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "created": 1677610602, "model": "claude-3-haiku"}'.encode(), + request=request ) - - # Verify result is transformed to Responses API schema - assert response.model == "neosantara/claude-3-haiku" - assert response.output[0].content[0].text == "Hello from bridge" - assert hasattr(response, "created_at") -@pytest.mark.asyncio + mock_send.side_effect = side_effect + + response = litellm.responses( + model="neosantara/claude-3-haiku", + input="hi from responses", + api_key="sk-1234" + ) + + # Verify result is transformed to Responses API schema + # Bridge transforms choices[0].message.content -> output[0].content[0].text + assert response.model == "neosantara/claude-3-haiku" + assert response.output[0].content[0].text == "Hello from bridge" + assert hasattr(response, "created_at") + assert response.created_at == 1677610602 + +@pytest.mark.anyio async def test_neosantara_anthropic_messages_bridge(): """ Test that Neosantara works with litellm.anthropic_messages() API bridge. """ os.environ["NEOSANTARA_API_KEY"] = "sk-1234" - with pytest.MonkeyPatch().context() as m: - async def mock_async_send(self, request, **kwargs): + with patch("httpx.AsyncClient.send") as mock_async_send: + async def side_effect(request, **kwargs): # Verify the bridge transformed Anthropic messages to OpenAI messages body = json.loads(request.read()) assert "messages" in body @@ -146,11 +158,11 @@ async def mock_async_send(self, request, **kwargs): return httpx.Response( 200, - content='{"choices": [{"message": {"content": "Hello from anthropic bridge"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "id": "chatcmpl-123", "created": 1677610602}'.encode(), + content='{"id": "chatcmpl-123", "choices": [{"message": {"content": "Hello from anthropic bridge", "role": "assistant"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "created": 1677610602, "model": "claude-3-haiku"}'.encode(), request=request ) - m.setattr("httpx.AsyncClient.send", mock_async_send) + mock_async_send.side_effect = side_effect response = await litellm.anthropic_messages( model="neosantara/claude-3-haiku", @@ -159,12 +171,26 @@ async def mock_async_send(self, request, **kwargs): max_tokens=100 ) - # Check both dict and object access to be robust for Anthropic bridge + # Verify result is transformed to Anthropic Messages API schema + # Bridge transforms choices[0].message.content -> content[0].text + # If result is an object (AnthropicMessagesResponse), access via .content[0].text + # If result is a dict, access via ["content"][0]["text"] if isinstance(response, dict): + assert response["model"] == "neosantara/claude-3-haiku" assert response["role"] == "assistant" - assert response["content"][0].text == "Hello from anthropic bridge" + # Access text via object attribute if subscripting fails + content_block = response["content"][0] + if hasattr(content_block, "text"): + assert content_block.text == "Hello from anthropic bridge" + else: + assert content_block["text"] == "Hello from anthropic bridge" assert response["type"] == "message" + assert response["usage"]["input_tokens"] == 10 + assert response["usage"]["output_tokens"] == 10 else: + assert response.model == "neosantara/claude-3-haiku" assert response.role == "assistant" assert response.content[0].text == "Hello from anthropic bridge" assert response.type == "message" + assert response.usage.input_tokens == 10 + assert response.usage.output_tokens == 10 \ No newline at end of file From 526782b307e40dfa4958f730798bb00a6e8a1dee Mon Sep 17 00:00:00 2001 From: neosantara-xyz Date: Sat, 7 Feb 2026 15:42:50 +0000 Subject: [PATCH 7/8] fix(neosantara): remove redundant provider check in main.py --- litellm/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/litellm/main.py b/litellm/main.py index 3f87b80c7961..5644dab23e56 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -4755,7 +4755,6 @@ def embedding( # noqa: PLR0915 or custom_llm_provider == "together_ai" or custom_llm_provider == "nvidia_nim" or custom_llm_provider == "litellm_proxy" - or custom_llm_provider == "neosantara" or (model in litellm.open_ai_embedding_models and custom_llm_provider is None) or custom_llm_provider in litellm.openai_compatible_providers or JSONProviderRegistry.exists(custom_llm_provider) From 583f645ca12d8c7f31a7e552c79095adf772f85e Mon Sep 17 00:00:00 2001 From: ErRickow Date: Wed, 4 Mar 2026 05:43:14 +0000 Subject: [PATCH 8/8] feat(neosantara): integrate neosantara as openai-compatible provider, fix detection logic and update tests --- litellm/constants.py | 1 - .../get_llm_provider_logic.py | 18 +- provider_endpoints_support.json | 28 +- tests/test_litellm/test_neosantara.py | 239 +++++++++++------- 4 files changed, 175 insertions(+), 111 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index ae695fb89357..14c0fa97292f 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -608,7 +608,6 @@ "https://ai-gateway.vercel.sh/v1", "https://api.inference.wandb.ai/v1", "https://api.clarifai.com/v2/ext/openai/v1", - "api.neosantara.xyz/v1", ] diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 8fe27755585f..dccd305f9018 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -195,6 +195,21 @@ def get_llm_provider( # noqa: PLR0915 return model, custom_llm_provider, dynamic_api_key, api_base # check if api base is a known openai compatible endpoint if api_base: + if "api.neosantara.xyz/v1" in api_base: + custom_llm_provider = "neosantara" + dynamic_api_key = get_secret_str("NEOSANTARA_API_KEY") + if api_base is not None and not isinstance(api_base, str): + raise Exception( + "api base needs to be a string. api_base={}".format(api_base) + ) + if dynamic_api_key is not None and not isinstance(dynamic_api_key, str): + raise Exception( + "dynamic_api_key needs to be a string. dynamic_api_key={}".format( + dynamic_api_key + ) + ) + return model, custom_llm_provider, dynamic_api_key, api_base + for endpoint in litellm.openai_compatible_endpoints: if endpoint in api_base: if endpoint == "api.perplexity.ai": @@ -304,9 +319,6 @@ def get_llm_provider( # noqa: PLR0915 elif endpoint == "https://api.inference.wandb.ai/v1": custom_llm_provider = "wandb" dynamic_api_key = get_secret_str("WANDB_API_KEY") - elif endpoint == "api.neosantara.xyz/v1": - custom_llm_provider = "neosantara" - dynamic_api_key = get_secret_str("NEOSANTARA_API_KEY") if api_base is not None and not isinstance(api_base, str): raise Exception( diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index a94463ce653a..614cf6ee136a 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -1475,19 +1475,27 @@ "messages": true, "responses": true, "embeddings": true, - "image_generations": false, - "audio_transcriptions": false, - "audio_speech": false, - "moderations": false, - "batches": false, + "image_generations": true, + "audio_transcriptions": true, + "audio_speech": true, + "moderations": true, + "batches": true, "rerank": false, + "container": true, + "compact": true, "a2a": true, "interactions": true, - "ocr": false, - "search": false, - "video_generations": false, - "compact": false, - "files": false + "vector_store_files": true, + "vector_stores_create": true, + "vector_stores_search": true, + "assistants": true, + "container_files": true, + "fine_tuning": true, + "image_variations": true, + "rag_ingest": true, + "rag_query": true, + "realtime": true, + "text_completion": true } }, "nlp_cloud": { diff --git a/tests/test_litellm/test_neosantara.py b/tests/test_litellm/test_neosantara.py index f800f0f8e468..08d7c04c26a4 100644 --- a/tests/test_litellm/test_neosantara.py +++ b/tests/test_litellm/test_neosantara.py @@ -1,9 +1,24 @@ -import sys, os +import os import pytest import litellm import httpx import json from unittest.mock import MagicMock, patch +from openai.types.chat import ChatCompletion +from openai.types.chat.chat_completion import Choice +from openai.types.chat.chat_completion_message import ChatCompletionMessage +from openai.types.completion_usage import CompletionUsage +from openai.types.create_embedding_response import CreateEmbeddingResponse +from openai.types.embedding import Embedding + +# This class mimics the structure of the `APIResponse` object returned by `openai`'s `with_raw_response` +class MockAPIResponse: + def __init__(self, mock_obj): + self._mock_obj = mock_obj + self.headers = {"x-request-id": "some-id"} + + def parse(self): + return self._mock_obj def test_neosantara_provider_info(): """ @@ -41,92 +56,121 @@ def test_neosantara_auto_detection_api_base_no_protocol(): assert api_base == "api.neosantara.xyz/v1" assert model == "claude-3-haiku" -@patch("httpx.Client.send") -def test_neosantara_completion_formatting(mock_send): +@patch("openai.resources.chat.completions.Completions.create") +def test_neosantara_completion_formatting(mock_create): """ Test that a completion call to Neosantara formats the request correctly as an OpenAI-like call. """ os.environ["NEOSANTARA_API_KEY"] = "sk-1234" - - def side_effect(request, **kwargs): - # Verify request URL and headers - assert "api.neosantara.xyz" in str(request.url) - assert request.headers["Authorization"] == "Bearer sk-1234" - - # Verify request body is OpenAI format - body = json.loads(request.read()) - assert "messages" in body - assert body["model"] == "claude-3-haiku" - - return httpx.Response( - 200, - content='{"id": "chatcmpl-123", "choices": [{"message": {"content": "Hello world", "role": "assistant"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "created": 1677610602, "model": "claude-3-haiku"}'.encode(), - request=request + + # Define the mock response object to be returned by the patched method + mock_choice = Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage( + content="Hello world", + role="assistant", + function_call=None, + tool_calls=None, ) + ) + mock_usage = CompletionUsage( + completion_tokens=10, + prompt_tokens=10, + total_tokens=20 + ) + mock_chat_completion = ChatCompletion( + id="chatcmpl-123", + choices=[mock_choice], + created=1677610602, + model="claude-3-haiku", + object="chat.completion", + usage=mock_usage, + system_fingerprint=None + ) - mock_send.side_effect = side_effect + mock_create.return_value = MockAPIResponse(mock_chat_completion) response = litellm.completion( model="neosantara/claude-3-haiku", messages=[{"role": "user", "content": "hi"}], api_key="sk-1234" ) - + + # Assert that the mocked 'create' method was called with the correct parameters + mock_create.assert_called_once() + called_args, called_kwargs = mock_create.call_args + assert called_kwargs["model"] == "claude-3-haiku" + assert called_kwargs["messages"] == [{"role": "user", "content": "hi"}] + + # Assertions on the transformed response assert response.choices[0].message.content == "Hello world" assert response._hidden_params["custom_llm_provider"] == "neosantara" assert response._hidden_params["api_base"] == "https://api.neosantara.xyz/v1" - -@patch("httpx.Client.send") -def test_neosantara_embedding_formatting(mock_send): +@patch("openai.resources.embeddings.Embeddings.create") +def test_neosantara_embedding_formatting(mock_create): """ Test that an embedding call to Neosantara formats the request correctly as an OpenAI-like call. """ os.environ["NEOSANTARA_API_KEY"] = "sk-1234" - - def side_effect(request, **kwargs): - # Verify request URL - assert "api.neosantara.xyz" in str(request.url) - assert "/v1/embeddings" in str(request.url) - - return httpx.Response( - 200, - content='{"data": [{"embedding": [0.1, 0.2, 0.3], "index": 0, "object": "embedding"}], "model": "nusa-embedding-0001", "object": "list", "usage": {"prompt_tokens": 10, "total_tokens": 10}}'.encode(), - request=request - ) - mock_send.side_effect = side_effect + mock_embedding = Embedding( + embedding=[0.1, 0.2, 0.3], + index=0, + object="embedding" + ) + mock_response = CreateEmbeddingResponse( + data=[mock_embedding], + model="nusa-embedding-0001", + object="list", + usage={"prompt_tokens": 10, "total_tokens": 10} + ) + + mock_create.return_value = MockAPIResponse(mock_response) response = litellm.embedding( model="neosantara/nusa-embedding-0001", input=["hi"], api_key="sk-1234" ) - + assert response.data[0]["embedding"] == [0.1, 0.2, 0.3] assert response._hidden_params["custom_llm_provider"] == "neosantara" assert response._hidden_params["api_base"] == "https://api.neosantara.xyz/v1" - -@patch("httpx.Client.send") -def test_neosantara_responses_api_bridge(mock_send): +@patch("openai.resources.chat.completions.Completions.create") +def test_neosantara_responses_api_bridge(mock_create): """ Test that Neosantara works with litellm.responses() API bridge. """ os.environ["NEOSANTARA_API_KEY"] = "sk-1234" - def side_effect(request, **kwargs): - # Verify the bridge transformed 'input' into OpenAI 'messages' - body = json.loads(request.read()) - assert "messages" in body - assert body["messages"][0]["content"] == "hi from responses" - assert "/v1/chat/completions" in str(request.url) - - return httpx.Response( - 200, - content='{"id": "chatcmpl-123", "choices": [{"message": {"content": "Hello from bridge", "role": "assistant"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "created": 1677610602, "model": "claude-3-haiku"}'.encode(), - request=request + # Define the mock response object to be returned by the patched method + mock_choice = Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage( + content="Hello from bridge", + role="assistant", + function_call=None, + tool_calls=None, ) + ) + mock_usage = CompletionUsage( + completion_tokens=10, + prompt_tokens=10, + total_tokens=20 + ) + mock_chat_completion = ChatCompletion( + id="chatcmpl-123", + choices=[mock_choice], + created=1677610602, + model="claude-3-haiku", + object="chat.completion", + usage=mock_usage, + system_fingerprint=None + ) - mock_send.side_effect = side_effect + mock_create.return_value = MockAPIResponse(mock_chat_completion) response = litellm.responses( model="neosantara/claude-3-haiku", @@ -142,55 +186,56 @@ def side_effect(request, **kwargs): assert response.created_at == 1677610602 @pytest.mark.anyio -async def test_neosantara_anthropic_messages_bridge(): +@patch("openai.resources.chat.completions.AsyncCompletions.create") +async def test_neosantara_anthropic_messages_bridge(mock_async_create): """ Test that Neosantara works with litellm.anthropic_messages() API bridge. """ os.environ["NEOSANTARA_API_KEY"] = "sk-1234" - with patch("httpx.AsyncClient.send") as mock_async_send: - async def side_effect(request, **kwargs): - # Verify the bridge transformed Anthropic messages to OpenAI messages - body = json.loads(request.read()) - assert "messages" in body - assert body["messages"][0]["role"] == "user" - assert "/v1/chat/completions" in str(request.url) - - return httpx.Response( - 200, - content='{"id": "chatcmpl-123", "choices": [{"message": {"content": "Hello from anthropic bridge", "role": "assistant"}, "finish_reason": "stop", "index": 0}], "usage": {"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20}, "object": "chat.completion", "created": 1677610602, "model": "claude-3-haiku"}'.encode(), - request=request - ) - - mock_async_send.side_effect = side_effect - - response = await litellm.anthropic_messages( - model="neosantara/claude-3-haiku", - messages=[{"role": "user", "content": "hi from anthropic"}], - api_key="sk-1234", - max_tokens=100 + # Define the mock response object to be returned by the patched method + mock_choice = Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage( + content="Hello from anthropic bridge", + role="assistant", + function_call=None, + tool_calls=None, ) - - # Verify result is transformed to Anthropic Messages API schema - # Bridge transforms choices[0].message.content -> content[0].text - # If result is an object (AnthropicMessagesResponse), access via .content[0].text - # If result is a dict, access via ["content"][0]["text"] - if isinstance(response, dict): - assert response["model"] == "neosantara/claude-3-haiku" - assert response["role"] == "assistant" - # Access text via object attribute if subscripting fails - content_block = response["content"][0] - if hasattr(content_block, "text"): - assert content_block.text == "Hello from anthropic bridge" - else: - assert content_block["text"] == "Hello from anthropic bridge" - assert response["type"] == "message" - assert response["usage"]["input_tokens"] == 10 - assert response["usage"]["output_tokens"] == 10 - else: - assert response.model == "neosantara/claude-3-haiku" - assert response.role == "assistant" - assert response.content[0].text == "Hello from anthropic bridge" - assert response.type == "message" - assert response.usage.input_tokens == 10 - assert response.usage.output_tokens == 10 \ No newline at end of file + ) + mock_usage = CompletionUsage( + completion_tokens=10, + prompt_tokens=10, + total_tokens=20 + ) + mock_chat_completion = ChatCompletion( + id="chatcmpl-123", + choices=[mock_choice], + created=1677610602, + model="claude-3-haiku", + object="chat.completion", + usage=mock_usage, + system_fingerprint=None + ) + + # For async, the mock's return value should be an awaitable + async def side_effect(*args, **kwargs): + return MockAPIResponse(mock_chat_completion) + + mock_async_create.side_effect = side_effect + + response = await litellm.anthropic_messages( + model="neosantara/claude-3-haiku", + messages=[{"role": "user", "content": "hi from anthropic"}], + api_key="sk-1234", + max_tokens=100 + ) + + # Verify result is transformed to Anthropic Messages API schema + assert response["model"] == "neosantara/claude-3-haiku" + assert response["role"] == "assistant" + assert response["content"][0].text == "Hello from anthropic bridge" + assert response["type"] == "message" + assert response["usage"]["input_tokens"] == 10 + assert response["usage"]["output_tokens"] == 10 \ No newline at end of file