Add digitalocean provider

This commit is contained in:
Muhammad Sannan Nasir 2025-06-30 20:48:22 +05:00
parent f196434d55
commit 96dda40f45
17 changed files with 1396 additions and 796 deletions

View file

@ -1154,7 +1154,7 @@ jobs:
name: Install Helm
command: |
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Install kind
- run:
name: Install Kind
@ -1162,7 +1162,7 @@ jobs:
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
# Install kubectl
- run:
name: Install kubectl
@ -1170,19 +1170,19 @@ jobs:
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
# Create kind cluster
- run:
name: Create Kind Cluster
command: |
kind create cluster --name litellm-test
# Run helm lint
- run:
name: Run helm lint
command: |
helm lint ./deploy/charts/litellm-helm
# Run helm tests
- run:
name: Run helm tests
@ -1191,15 +1191,15 @@ jobs:
# Wait for pod to be ready
echo "Waiting 30 seconds for pod to be ready..."
sleep 30
# Print pod logs before running tests
echo "Printing pod logs..."
kubectl logs $(kubectl get pods -l app.kubernetes.io/name=litellm -o jsonpath="{.items[0].metadata.name}")
# Run the helm tests
helm test litellm --logs
helm test litellm --logs
# Cleanup
- run:
name: Cleanup
@ -1309,7 +1309,7 @@ jobs:
echo "=== Printing Full Container Startup Logs ==="
docker logs my-app
echo "=== End of Full Container Startup Logs ==="
if docker logs my-app 2>&1 | grep -q "prisma schema out of sync with db. Consider running these sql_commands to sync the two"; then
echo "Expected message found in logs. Test passed."
else
@ -1427,6 +1427,7 @@ jobs:
-e LANGFUSE_PROJECT2_PUBLIC=$LANGFUSE_PROJECT2_PUBLIC \
-e LANGFUSE_PROJECT1_SECRET=$LANGFUSE_PROJECT1_SECRET \
-e LANGFUSE_PROJECT2_SECRET=$LANGFUSE_PROJECT2_SECRET \
-e DIGITALOCEAN_API_KEY=$DIGITALOCEAN_API_KEY \
--name my-app \
-v $(pwd)/proxy_server_config.yaml:/app/config.yaml \
my-app:latest \
@ -1552,6 +1553,7 @@ jobs:
-e LANGFUSE_PROJECT2_PUBLIC=$LANGFUSE_PROJECT2_PUBLIC \
-e LANGFUSE_PROJECT1_SECRET=$LANGFUSE_PROJECT1_SECRET \
-e LANGFUSE_PROJECT2_SECRET=$LANGFUSE_PROJECT2_SECRET \
-e DIGITALOCEAN_API_KEY=$DIGITALOCEAN_API_KEY \
--name my-app \
-v $(pwd)/litellm/proxy/example_config_yaml/oai_misc_config.yaml:/app/config.yaml \
my-app:latest \
@ -2030,7 +2032,7 @@ jobs:
no_output_timeout:
120m
# Clean up first container
proxy_build_from_pip_tests:
# Change from docker to machine executor
machine:
@ -2235,17 +2237,17 @@ jobs:
curl -sSL https://rvm.io/mpapis.asc | gpg --import -
curl -sSL https://rvm.io/pkuczynski.asc | gpg --import -
}
# Install Ruby version manager (RVM)
curl -sSL https://get.rvm.io | bash -s stable
# Source RVM from the correct location
source $HOME/.rvm/scripts/rvm
# Install Ruby 3.2.2
rvm install 3.2.2
rvm use 3.2.2 --default
# Install latest Bundler
gem install bundler
@ -2408,32 +2410,32 @@ jobs:
python -m pip install toml
# Get current version from pyproject.toml
CURRENT_VERSION=$(python -c "import toml; print(toml.load('pyproject.toml')['tool']['poetry']['version'])")
# Get last published version from PyPI
LAST_VERSION=$(curl -s https://pypi.org/pypi/litellm-proxy-extras/json | python -c "import json, sys; print(json.load(sys.stdin)['info']['version'])")
echo "Current version: $CURRENT_VERSION"
echo "Last published version: $LAST_VERSION"
# Compare versions using Python's packaging.version
VERSION_COMPARE=$(python -c "from packaging import version; print(1 if version.parse('$CURRENT_VERSION') < version.parse('$LAST_VERSION') else 0)")
echo "Version compare: $VERSION_COMPARE"
if [ "$VERSION_COMPARE" = "1" ]; then
echo "Error: Current version ($CURRENT_VERSION) is less than last published version ($LAST_VERSION)"
exit 1
fi
# If versions are equal or current is greater, check contents
pip download --no-deps litellm-proxy-extras==$LAST_VERSION -d /tmp
echo "Contents of /tmp directory:"
ls -la /tmp
# Find the downloaded file (could be .whl or .tar.gz)
DOWNLOADED_FILE=$(ls /tmp/litellm_proxy_extras-*)
echo "Downloaded file: $DOWNLOADED_FILE"
# Extract based on file extension
if [[ "$DOWNLOADED_FILE" == *.whl ]]; then
echo "Extracting wheel file..."
@ -2444,10 +2446,10 @@ jobs:
tar -xzf "$DOWNLOADED_FILE" -C /tmp
EXTRACTED_DIR="/tmp/litellm_proxy_extras-$LAST_VERSION"
fi
echo "Contents of extracted package:"
ls -R "$EXTRACTED_DIR"
# Compare contents
if ! diff -r "$EXTRACTED_DIR/litellm_proxy_extras" ./litellm_proxy_extras; then
if [ "$CURRENT_VERSION" = "$LAST_VERSION" ]; then
@ -2513,16 +2515,16 @@ jobs:
export NVM_DIR="/opt/circleci/.nvm"
source "$NVM_DIR/nvm.sh"
source "$NVM_DIR/bash_completion"
# Install and use Node version
nvm install v18.17.0
nvm use v18.17.0
cd ui/litellm-dashboard
# Install dependencies first
npm install
# Now source the build script
source ./build_ui.sh
- run:
@ -2569,7 +2571,7 @@ jobs:
name: Install Playwright Browsers
command: |
npx playwright install
- run:
name: Build Docker image
command: docker build -t my-app:latest -f ./docker/Dockerfile.database .
@ -2933,4 +2935,4 @@ workflows:
- proxy_pass_through_endpoint_tests
- check_code_and_doc_quality
- publish_proxy_extras

View file

@ -44,7 +44,7 @@ Support for more providers. Missing a provider or LLM Platform, raise a [feature
# Usage ([**Docs**](https://docs.litellm.ai/docs/))
> [!IMPORTANT]
> LiteLLM v1.0.0 now requires `openai>=1.0.0`. Migration guide [here](https://docs.litellm.ai/docs/migration)
> LiteLLM v1.0.0 now requires `openai>=1.0.0`. Migration guide [here](https://docs.litellm.ai/docs/migration)
> LiteLLM v1.40.14+ now requires `pydantic>=2.0.0`. No changes required.
<a target="_blank" href="https://colab.research.google.com/github/BerriAI/litellm/blob/main/cookbook/liteLLM_Getting_Started.ipynb">
@ -129,7 +129,7 @@ print(response)
## Streaming ([Docs](https://docs.litellm.ai/docs/completion/stream))
liteLLM supports streaming the model response back, pass `stream=True` to get a streaming iterator in response.
liteLLM supports streaming the model response back, pass `stream=True` to get a streaming iterator in response.
Streaming is supported for all models (Bedrock, Huggingface, TogetherAI, Azure, OpenAI, etc.)
```python
@ -227,7 +227,7 @@ $ litellm --model huggingface/bigcode/starcoder
> [!IMPORTANT]
> 💡 [Use LiteLLM Proxy with Langchain (Python, JS), OpenAI SDK (Python, JS) Anthropic SDK, Mistral SDK, LlamaIndex, Instructor, Curl](https://docs.litellm.ai/docs/proxy/user_keys)
> 💡 [Use LiteLLM Proxy with Langchain (Python, JS), OpenAI SDK (Python, JS) Anthropic SDK, Mistral SDK, LlamaIndex, Instructor, Curl](https://docs.litellm.ai/docs/proxy/user_keys)
```python
import openai # openai v1.0.0+
@ -259,7 +259,7 @@ echo 'LITELLM_MASTER_KEY="sk-1234"' > .env
# Add the litellm salt key - you cannot change this after adding a model
# It is used to encrypt / decrypt your LLM API Key credentials
# We recommend - https://1password.com/password-generator/
# We recommend - https://1password.com/password-generator/
# password generator to get a random hash for litellm salt key
echo 'LITELLM_SALT_KEY="sk-1234"' > .env
@ -332,6 +332,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) | ✅ | ✅ | ✅ | ✅ | | |
| [DigitalOcean](https://docs.litellm.ai/docs/providers/digitalocean) | ✅ | ✅ | | | | |
[**Read the Docs**](https://docs.litellm.ai/docs/)
@ -344,7 +345,7 @@ For companies that need better security, user management and professional suppor
[Talk to founders](https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat)
This covers:
This covers:
- ✅ **Features under the [LiteLLM Commercial License](https://docs.litellm.ai/docs/proxy/enterprise):**
- ✅ **Feature Prioritization**
- ✅ **Custom Integrations**
@ -356,7 +357,7 @@ This covers:
LiteLLM follows the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html).
We run:
We run:
- Ruff for [formatting and linting checks](https://github.com/BerriAI/litellm/blob/e19bb55e3b4c6a858b6e364302ebbf6633a51de5/.circleci/config.yml#L320)
- Mypy + Pyright for typing [1](https://github.com/BerriAI/litellm/blob/e19bb55e3b4c6a858b6e364302ebbf6633a51de5/.circleci/config.yml#L90), [2](https://github.com/BerriAI/litellm/blob/e19bb55e3b4c6a858b6e364302ebbf6633a51de5/.pre-commit-config.yaml#L4)
- Black for [formatting](https://github.com/BerriAI/litellm/blob/e19bb55e3b4c6a858b6e364302ebbf6633a51de5/.circleci/config.yml#L79)

View file

@ -4,6 +4,6 @@ COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
EXPOSE $PORT
EXPOSE 3000
CMD litellm --host 0.0.0.0 --port $PORT --workers 10 --config config.yaml
CMD litellm --host 0.0.0.0 --port 3000 --workers 10 --config config.yaml

View file

@ -245,7 +245,8 @@ const sidebars = {
"providers/sambanova",
"providers/custom_llm_server",
"providers/petals",
"providers/snowflake"
"providers/snowflake",
"providers/digitalocean",
],
},
{

View file

@ -193,6 +193,7 @@ baseten_key: Optional[str] = None
aleph_alpha_key: Optional[str] = None
nlp_cloud_key: Optional[str] = None
snowflake_key: Optional[str] = None
digitalocean_api_key: Optional[str] = None
common_cloud_provider_auth_params: dict = {
"params": ["project", "region_name", "token"],
"providers": ["vertex_ai", "bedrock", "watsonx", "azure", "vertex_ai_beta"],
@ -431,6 +432,7 @@ galadriel_models: List = []
sambanova_models: List = []
assemblyai_models: List = []
snowflake_models: List = []
digitalocean_models: List = []
def is_bedrock_pricing_only_model(key: str) -> bool:
@ -588,6 +590,8 @@ def add_known_models():
jina_ai_models.append(key)
elif value.get("litellm_provider") == "snowflake":
snowflake_models.append(key)
elif value.get("litellm_provider") == "digitalocean":
digitalocean_models.append(key)
add_known_models()
@ -664,6 +668,7 @@ model_list = (
+ assemblyai_models
+ jina_ai_models
+ snowflake_models
+ digitalocean_models
)
model_list_set = set(model_list)
@ -721,6 +726,7 @@ models_by_provider: dict = {
"assemblyai": assemblyai_models,
"jina_ai": jina_ai_models,
"snowflake": snowflake_models,
"digitalocean": digitalocean_models,
}
# mapping for those models which have larger equivalents
@ -963,7 +969,7 @@ from .llms.openai.chat.o_series_transformation import (
)
from .llms.snowflake.chat.transformation import SnowflakeConfig
from .llms.digitalocean.chat.transformation import DigitalOceanConfig
openaiOSeriesConfig = OpenAIOSeriesConfig()
from .llms.openai.chat.gpt_transformation import (
OpenAIGPTConfig,

View file

@ -157,6 +157,7 @@ LITELLM_CHAT_PROVIDERS = [
"hosted_vllm",
"lm_studio",
"galadriel",
"digitalocean"
]

View file

@ -321,6 +321,8 @@ def get_llm_provider( # noqa: PLR0915
custom_llm_provider = "openai"
elif model in litellm.empower_models:
custom_llm_provider = "empower"
elif model in litellm.digitalocean_models:
custom_llm_provider = "digitalocean"
elif model == "*":
custom_llm_provider = "openai"
if not custom_llm_provider:
@ -580,6 +582,13 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915
or f"https://{get_secret('SNOWFLAKE_ACCOUNT_ID')}.snowflakecomputing.com/api/v2/cortex/inference:complete"
) # type: ignore
dynamic_api_key = api_key or get_secret_str("SNOWFLAKE_JWT")
elif custom_llm_provider == "digitalocean":
(
api_base,
dynamic_api_key,
) = litellm.DigitalOceanConfig()._get_openai_compatible_provider_info(
api_base, 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))

View file

@ -0,0 +1,191 @@
"""
Translate from OpenAI's `/v1/chat/completions` to DigitalOcean AI Agent's `/v1/chat/completions`
"""
from typing import List, Optional, Tuple, Union, Dict
from pydantic import BaseModel
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import (
AllMessageValues,
ChatCompletionAssistantMessage,
ChatCompletionToolParam,
ChatCompletionToolParamFunctionChunk,
)
from ...openai_like.chat.transformation import OpenAILikeChatConfig
class DigitalOceanConfig(OpenAILikeChatConfig):
# DigitalOcean-specific parameters:
stream_options: Optional[Dict] = None
kb_filters: Optional[List[Dict]] = None
filter_kb_content_by_query_metadata: Optional[bool] = None
instruction_override: Optional[str] = None
include_functions_info: Optional[bool] = None
include_retrieval_info: Optional[bool] = None
include_guardrails_info: Optional[bool] = None
provide_citations: Optional[bool] = None
def __init__(
self,
frequency_penalty: Optional[float] = None,
function_call: Optional[Union[str, Dict]] = None,
functions: Optional[List] = None,
logit_bias: Optional[Dict] = None,
max_tokens: Optional[int] = None,
max_completion_tokens: Optional[int] = None, # Additional field; interchangeable with max_tokens per spec
n: Optional[int] = None,
presence_penalty: Optional[float] = None,
retrieval_method: Optional[str] = None, # expected values: "rewrite", "step_back", "sub_queries", "none"
stop: Optional[Union[str, List[str]]] = None,
stream: Optional[bool] = None,
temperature: Optional[float] = None,
top_p: Optional[float] = None,
response_format: Optional[Dict] = None,
tools: Optional[List] = None,
tool_choice: Optional[Union[str, Dict]] = None,
) -> None:
locals_ = locals().copy()
for key, value in locals_.items():
if key != "self" and value is not None:
setattr(self.__class__, key, value)
@classmethod
def get_config(cls):
return super().get_config()
def get_supported_openai_params(self, model: str) -> list:
base_params = super().get_supported_openai_params(model)
try:
base_params.remove("max_retries")
except ValueError:
pass
return base_params
def validate_environment(self,
headers: dict,
model: str,
messages: List[AllMessageValues],
optional_params: dict,
litellm_params: dict,
api_key: Optional[str] = None,
api_base: Optional[str] = None):
api_key = api_key or get_secret_str("DIGITALOCEAN_API_KEY")
if api_key is None:
raise ValueError("DigitalOcean API key not found")
if headers is None:
headers = {}
headers["Authorization"] = f"Bearer {api_key}"
headers["Content-Type"] = "application/json"
return headers
def get_complete_url(
self,
api_base: Optional[str],
api_key: Optional[str],
model: str,
optional_params: dict,
litellm_params: dict,
stream: Optional[bool] = None,
) -> str:
complete_url = f"{api_base}/api/v1/chat/completions"
return complete_url
def _transform_messages(self, messages: List[AllMessageValues], model: str) -> List:
for idx, message in enumerate(messages):
"""
1. Don't pass 'null' function_call assistant message to groq - https://github.com/BerriAI/litellm/issues/5839
"""
if isinstance(message, BaseModel):
_message = message.model_dump()
else:
_message = message
assistant_message = _message.get("role") == "assistant"
if assistant_message:
new_message = ChatCompletionAssistantMessage(role="assistant")
for k, v in _message.items():
if v is not None:
new_message[k] = v # type: ignore
messages[idx] = new_message
return messages
def _get_openai_compatible_provider_info(
self, api_base: Optional[str], api_key: Optional[str]
) -> Tuple[Optional[str], Optional[str]]:
api_base = (
api_base
or get_secret_str("DIGITALOCEAN_AGENT_ENDPOINT")
or get_secret_str("DO_AGENT_ENDPOINT")
) # type: ignore
dynamic_api_key = api_key or get_secret_str("DO_API_KEY") or get_secret_str("DIGITALOCEAN_API_KEY")
return api_base, dynamic_api_key
def _create_json_tool_call_for_response_format(
self,
json_schema: dict,
):
"""
Handles creating a tool call for getting responses in JSON format.
Args:
json_schema (Optional[dict]): The JSON schema the response should be in
Returns:
AnthropicMessagesTool: The tool call to send to Anthropic API to get responses in JSON format
"""
return ChatCompletionToolParam(
type="function",
function=ChatCompletionToolParamFunctionChunk(
name="json_tool_call",
parameters=json_schema,
),
)
def map_openai_params(
self,
non_default_params: dict,
optional_params: dict,
model: str,
drop_params: bool = False,
replace_max_completion_tokens_with_max_tokens: bool = False, # groq supports max_completion_tokens
) -> dict:
_response_format = non_default_params.get("response_format")
if _response_format is not None and isinstance(_response_format, dict):
json_schema: Optional[dict] = None
if "response_schema" in _response_format:
json_schema = _response_format["response_schema"]
elif "json_schema" in _response_format:
json_schema = _response_format["json_schema"]["schema"]
"""
When using tools in this way: - https://docs.anthropic.com/en/docs/build-with-claude/tool-use#json-mode
- You usually want to provide a single tool
- You should set tool_choice (see Forcing tool use) to instruct the model to explicitly use that tool
- Remember that the model will pass the input to the tool, so the name of the tool and description should be from the model’s perspective.
"""
if json_schema is not None:
_tool_choice = {
"type": "function",
"function": {"name": "json_tool_call"},
}
_tool = self._create_json_tool_call_for_response_format(
json_schema=json_schema,
)
optional_params["tools"] = [_tool]
optional_params["tool_choice"] = _tool_choice
optional_params["json_mode"] = True
non_default_params.pop(
"response_format", None
) # only remove if it's a json_schema - handled via using groq's tool calling params.
optional_params = super().map_openai_params(
non_default_params, optional_params, model, drop_params
)
return optional_params

View file

@ -3065,6 +3065,26 @@ def completion( # type: ignore # noqa: PLR0915
additional_args={"headers": headers},
)
raise e
elif custom_llm_provider == "digitalocean":
api_base = litellm.api_base or api_base
response = base_llm_http_handler.completion(
model=model,
stream=stream,
messages=messages,
acompletion=acompletion,
api_base=api_base,
model_response=model_response,
optional_params=optional_params,
litellm_params=litellm_params,
custom_llm_provider="digitalocean",
timeout=timeout,
headers=headers,
encoding=encoding,
api_key=api_key,
logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
client=client,
)
elif custom_llm_provider == "custom":
url = litellm.api_base or api_base or ""

View file

@ -2102,6 +2102,7 @@ class LlmProviders(str, Enum):
TOPAZ = "topaz"
ASSEMBLYAI = "assemblyai"
SNOWFLAKE = "snowflake"
DIGITALOCEAN = "digitalocean"
# Create a set of all provider values for quick lookup

View file

@ -3705,6 +3705,17 @@ def get_optional_params( # noqa: PLR0915
else False
),
)
elif custom_llm_provider == "digitalocean":
optional_params = litellm.DigitalOceanConfig().map_openai_params(
non_default_params=non_default_params,
optional_params=optional_params,
model=model,
drop_params=(
drop_params
if drop_params is not None and isinstance(drop_params, bool)
else False
),
)
elif custom_llm_provider == "openai":
optional_params = litellm.OpenAIConfig().map_openai_params(
non_default_params=non_default_params,
@ -6569,6 +6580,8 @@ class ProviderConfigManager:
return litellm.LiteLLMProxyChatConfig()
elif litellm.LlmProviders.OPENAI == provider:
return litellm.OpenAIGPTConfig()
elif litellm.LlmProviders.DIGITALOCEAN == provider:
return litellm.DigitalOceanConfig()
return None
@staticmethod

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@ model_list:
api_version: "2023-05-15"
api_key: os.environ/AZURE_API_KEY # The `os.environ/` prefix tells litellm to read this from the env. See https://docs.litellm.ai/docs/simple_proxy#load-api-keys-from-vault
- model_name: gpt-3.5-turbo-large
litellm_params:
litellm_params:
model: "gpt-3.5-turbo-1106"
api_key: os.environ/OPENAI_API_KEY
rpm: 480
@ -36,9 +36,9 @@ model_list:
- model_name: sagemaker-completion-model
litellm_params:
model: sagemaker/berri-benchmarking-Llama-2-70b-chat-hf-4
input_cost_per_second: 0.000420
input_cost_per_second: 0.000420
- model_name: text-embedding-ada-002
litellm_params:
litellm_params:
model: azure/azure-embedding-model
api_key: os.environ/AZURE_API_KEY
api_base: https://openai-gpt-4-test-v-1.openai.azure.com/
@ -106,7 +106,7 @@ model_list:
litellm_params:
model: openai/*
api_key: os.environ/OPENAI_API_KEY
# provider specific wildcard routing
- model_name: "anthropic/*"
@ -137,18 +137,23 @@ model_list:
model: openai/my-fake-model
api_key: my-fake-key
api_base: https://exampleopenaiendpoint-production.up.railway.appxxxx/
- model_name: "digitalocean/*"
litellm_params:
model: "digitalocean/*"
api_key: os.environ/DIGITALOCEAN_API_KEY
api_base: os.environ/DIGITALOCEAN_AGENT_ENDPOINT
litellm_settings:
# set_verbose: True # Uncomment this if you want to see verbose logs; not recommended in production
drop_params: True
# max_budget: 100
# max_budget: 100
# budget_duration: 30d
num_retries: 5
request_timeout: 600
telemetry: False
context_window_fallbacks: [{"gpt-3.5-turbo": ["gpt-3.5-turbo-large"]}]
default_team_settings:
default_team_settings:
- team_id: team-1
success_callback: ["langfuse"]
failure_callback: ["langfuse"]
@ -180,14 +185,14 @@ files_settings:
api_key: os.environ/OPENAI_API_KEY
router_settings:
routing_strategy: usage-based-routing-v2
routing_strategy: usage-based-routing-v2
redis_host: os.environ/REDIS_HOST
redis_password: os.environ/REDIS_PASSWORD
redis_port: os.environ/REDIS_PORT
enable_pre_call_checks: true
model_group_alias: {"my-special-fake-model-alias-name": "fake-openai-endpoint-3"}
model_group_alias: {"my-special-fake-model-alias-name": "fake-openai-endpoint-3"}
general_settings:
general_settings:
master_key: sk-1234 # [OPTIONAL] Use to enforce auth on proxy. See - https://docs.litellm.ai/docs/proxy/virtual_keys
store_model_in_db: True
proxy_budget_rescheduler_min_time: 60
@ -200,7 +205,7 @@ general_settings:
- path: "/v1/rerank" # route you want to add to LiteLLM Proxy Server
target: "https://api.cohere.com/v1/rerank" # URL this route should forward requests to
headers: # headers to forward to this URL
content-type: application/json # (Optional) Extra Headers to pass to this endpoint
content-type: application/json # (Optional) Extra Headers to pass to this endpoint
accept: application/json
forward_headers: True
@ -208,4 +213,4 @@ general_settings:
# settings for using redis caching
# REDIS_HOST: redis-16337.c322.us-east-1-2.ec2.cloud.redislabs.com
# REDIS_PORT: "16337"
# REDIS_PASSWORD:
# REDIS_PASSWORD:

View file

@ -0,0 +1,60 @@
import os
import sys
import pytest
sys.path.insert(
0, os.path.abspath("../../../../..")
) # Adds the parent directory to the system path
from litellm.llms.digitalocean.chat.transformation import DigitalOceanConfig
DO_ENDPOINT_PATH = "/api/v1/chat/completions"
DO_BASE_URL = "https://api.digitalocean.com"
@pytest.fixture
def config():
return DigitalOceanConfig()
def test_validate_environment_sets_headers(monkeypatch, config):
monkeypatch.setenv("DIGITALOCEAN_API_KEY", "test-key")
headers = {}
result = config.validate_environment(
headers=headers,
model="digitalocean/test-model",
messages=[],
optional_params={},
litellm_params={},
api_key=None,
api_base=None,
)
assert result["Authorization"] == "Bearer test-key"
assert result["Content-Type"] == "application/json"
def test_get_complete_url(config):
url = config.get_complete_url(
api_base=DO_BASE_URL,
api_key="test-key",
model="digitalocean/test-model",
optional_params={},
litellm_params={},
stream=False,
)
assert url == f"{DO_BASE_URL}{DO_ENDPOINT_PATH}"
def test_transform_messages_handles_dicts_only(config):
messages = [
{"role": "assistant", "content": "Hello!"},
{"role": "user", "content": "Hi!"},
]
out = config._transform_messages(messages, model="digitalocean/test-model")
assert out[0]["role"] == "assistant"
assert out[0]["content"] == "Hello!"
assert out[1]["role"] == "user"
assert out[1]["content"] == "Hi!"
def test_get_openai_compatible_provider_info_env(monkeypatch, config):
monkeypatch.setenv("DIGITALOCEAN_AGENT_ENDPOINT", DO_BASE_URL)
monkeypatch.setenv("DIGITALOCEAN_API_KEY", "env-key")
api_base, api_key = config._get_openai_compatible_provider_info(None, None)
assert api_base == DO_BASE_URL
assert api_key == "env-key"

View file

@ -0,0 +1,229 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="68.544662mm"
height="68.542419mm"
viewBox="0 0 68.544665 68.542419"
version="1.1"
id="svg5"
inkscape:version="1.1.2 (b8e25be833, 2022-02-05)"
sodipodi:docname="DigitalOcean 2016 Icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.90583672"
inkscape:cx="219.68639"
inkscape:cy="198.71131"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="g11760" />
<defs
id="defs2">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath838">
<path
d="M 0,792 H 612 V 0 H 0 Z"
id="path836" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath858">
<path
d="M 0,792 H 612 V 0 H 0 Z"
id="path856" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath255">
<path
d="M 425.197,24.946 H 559.275 V 161.574 H 425.197 Z"
id="path253"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2816">
<path
d="M 0,0 H 396 V 612 H 0 Z"
id="path2814"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2794">
<path
d="M 0,0 H 396 V 612 H 0 Z"
id="path2792"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2778">
<path
d="M 0,0 H 396 V 612 H 0 Z"
id="path2776"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2748">
<path
d="M 0,0 H 396 V 612 H 0 Z"
id="path2746"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2710">
<path
d="M 0,0 H 396 V 612 H 0 Z"
id="path2708"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2664">
<path
d="M 0,0 H 396 V 612 H 0 Z"
id="path2662"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath6055">
<path
d="m 170.008,153.156 v 18.559 h -5.93 v -18.559 h 5.93"
id="path6053"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath6035">
<path
d="M 30,0 V 292 H 177 V 0 Z"
id="path6033"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath6025">
<path
d="m 170.008,153.156 v 18.559 h -5.93 v -18.559 h 5.93"
id="path6023"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath81">
<path
d="M 45.36,756.5 H 90.60001 V 806 H 45.36 Z"
id="path79"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath71">
<path
d="M 45,755.12 H 90.84 V 806 H 45 Z"
id="path69"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath7101">
<path
d="M 0,0 H 612 V 828 H 0 Z"
id="path7099"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath9113">
<path
d="M 0,531 H 719.972 V 0 H 0 Z"
id="path9111"
inkscape:connector-curvature="0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath11764">
<path
d="M 0,792 H 612 V 0 H 0 Z"
id="path11762"
inkscape:connector-curvature="0" />
</clipPath>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-23.73095,-45.75945)">
<g
id="g830"
inkscape:label="PSA-Style-Guide"
transform="matrix(0.35277777,0,0,-0.35277777,-34.820531,253.68695)">
<g
style="fill:none"
id="g2484"
transform="matrix(4.7902363,0,0,-4.7902363,168.73293,576.70634)">
<g
id="g11760"
clip-path="url(#clipPath11764)"
transform="matrix(0.92105933,0,0,-0.92105933,-111.34849,650.43322)">
<g
id="g11766"
transform="matrix(1.5371265,0,0,1.5371265,142.2855,665.02011)">
<path
d="m 0,0 v 5.547 c 5.888,0 10.439,5.828 8.192,12.017 -0.833,2.292 -2.661,4.121 -4.954,4.953 -6.189,2.246 -12.014,-2.305 -12.015,-8.191 0,0 0,-0.002 -0.001,-0.002 h -5.547 c 0,9.38 9.062,16.683 18.89,13.614 4.295,-1.34 7.708,-4.753 9.049,-9.048 C 16.683,9.061 9.379,0 0,0"
style="fill:#0069ff;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11768"
inkscape:connector-curvature="0" />
</g>
<g
id="g11770"
transform="matrix(1.5371265,0,0,1.5371265,142.30502,673.52534)">
<path
d="m 0,0 h -5.531 v 5.53 c 0,0 0,10e-4 10e-4,10e-4 h 5.529 L 0,5.53 Z"
style="fill:#0069ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
id="path11772"
inkscape:connector-curvature="0" />
</g>
<g
id="g11774"
transform="matrix(1.5371265,0,0,1.5371265,133.80133,666.99332)">
<path
d="M 0,0 H -4.249 L -4.25,0.001 V 4.25 H 0.001 V 0.001 Z"
style="fill:#0069ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
id="path11776"
inkscape:connector-curvature="0" />
</g>
<g
id="g11778"
transform="matrix(1.5371265,0,0,1.5371265,127.27377,673.52534)">
<path
d="m 0,0 h -3.562 c -0.001,0 -0.002,0.001 -0.002,0.001 v 3.56 c 0,0 10e-4,0.003 0.002,0.003 h 3.56 C -0.001,3.564 0,3.563 0,3.563 Z"
style="fill:#0069ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
id="path11780"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -327,7 +327,27 @@ const PROVIDER_CREDENTIAL_FIELDS: Record<Providers, ProviderCredentialField[]> =
label: "API Key",
type: "password",
required: true
}]
}],
[Providers.DigitalOcean]: [
{
key: "endpoint",
label: "DigitalOcean Endpoint",
placeholder: "https://...",
required: true
},
{
key: "base_model",
label: "Base Model",
placeholder: "digitalocean/mistral-nemo"
},
{
key: "api_key",
label: "DigitalOcean API Key",
type: "password",
required: true
}
]
};
const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
@ -335,7 +355,7 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
uploadProps
}) => {
const selectedProviderEnum = Providers[selectedProvider as keyof typeof Providers] as Providers;
// Simply use the fields as defined in PROVIDER_CREDENTIAL_FIELDS
const allFields = React.useMemo(() => {
return PROVIDER_CREDENTIAL_FIELDS[selectedProviderEnum] || [];
@ -353,7 +373,7 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
className={field.key === "vertex_credentials" ? "mb-0" : undefined}
>
{field.type === "select" ? (
<Select
<Select
placeholder={field.placeholder}
defaultValue={field.defaultValue}
>
@ -368,9 +388,9 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
<Button2 icon={<UploadOutlined />}>Click to Upload</Button2>
</Upload>
) : (
<TextInput
placeholder={field.placeholder}
type={field.type === "password" ? "password" : "text"}
<TextInput
placeholder={field.placeholder}
type={field.type === "password" ? "password" : "text"}
/>
)}
</Form.Item>

View file

@ -25,10 +25,9 @@ export enum Providers {
Perplexity = "Perplexity",
TogetherAI = "TogetherAI",
Openrouter = "Openrouter",
FireworksAI = "Fireworks AI"
FireworksAI = "Fireworks AI",
DigitalOcean = "DigitalOcean"
}
export const provider_map: Record<string, string> = {
OpenAI: "openai",
OpenAI_Text: "text-completion-openai",
@ -53,7 +52,8 @@ export const provider_map: Record<string, string> = {
Perplexity: "perplexity",
TogetherAI: "togetherai",
Openrouter: "openrouter",
FireworksAI: "fireworks_ai"
FireworksAI: "fireworks_ai",
DigitalOcean: "digitalocean"
};
const asset_logos_folder = '/ui/assets/logos/';
@ -82,7 +82,8 @@ export const providerLogoMap: Record<string, string> = {
[Providers.Sambanova]: `${asset_logos_folder}sambanova.svg`,
[Providers.TogetherAI]: `${asset_logos_folder}togetherai.svg`,
[Providers.Vertex_AI]: `${asset_logos_folder}google.svg`,
[Providers.xAI]: `${asset_logos_folder}xai.svg`
[Providers.xAI]: `${asset_logos_folder}xai.svg`,
[Providers.DigitalOcean]: `${asset_logos_folder}digitalocean.svg`
};
export const getProviderLogoAndName = (providerValue: string): { logo: string, displayName: string } => {
@ -136,9 +137,9 @@ export const getPlaceholder = (selectedProvider: string): string => {
console.log(`Provider key: ${providerKey}`);
let custom_llm_provider = provider_map[providerKey];
console.log(`Provider mapped to: ${custom_llm_provider}`);
let providerModels: Array<string> = [];
if (providerKey && typeof modelMap === "object") {
Object.entries(modelMap).forEach(([key, value]) => {
if (
@ -151,7 +152,7 @@ export const getPlaceholder = (selectedProvider: string): string => {
providerModels.push(key);
}
});
// Special case for cohere_chat
// we need both cohere_chat and cohere models to show on dropdown
if (providerKey == Providers.Cohere) {
@ -168,6 +169,6 @@ export const getPlaceholder = (selectedProvider: string): string => {
});
}
}
return providerModels;
};