v1.73.6.rc (#12146)

* fix - using on python 3.9

* [⚡️ Python SDK Import] - 2 second faster import times (#12135)

* speedup - move wb logger to conditional

* fix import path

* docs(index.md): initial pre-release note

* 🧹 Refactor init.py to use a model registry (#12138)

* fix - refactor init to use a registry

* # noqa: PLR0915

* fix import loc

* test whitelisted models

* Revert "🧹 Refactor init.py to use a model registry (#12138)" (#12141)

This reverts commit f93326a214.

* [⚡️ Python SDK import] - reduce python sdk import time by .3s  (#12140)

* use 1 file for KeyManagementSystem

* move key management settings

* fix import locs

* test_proxy_types_not_imported

* test the import loc

* fix import item

* fix imports

* fix import loc

* fix imports

* fix imports

* fix - revert list team changes

* fix for o-series param checks

* bump poetry

* docs(index.md): update release note with cleaner table for updated models

* `/v1/messages` - Remove hardcoded model name on streaming + Tags - enable setting custom header tags (#12131)

* fix(anthropic/experimental_pass_through): use given model name when returning streaming chunks

don't harcode model name on streaming

confusing for user

* fix(anthropic/streaming_iterator.py): remove scope of import

* feat(litellm_logging.py): allow admin to specify additional headers for using as spend tags

Closes https://github.com/BerriAI/litellm/issues/12129

* test(test_litellm_logging.py): add unit tests

* feat(openweb_ui.md): add custom tag tutorial to docs

* docs(cost_tracking.md): add tag based usage UI screenshot

* test: update test

* fix: fix import

* docs - update release notes

* Benefits of using gemini-cli with LiteLLM

* UI QA Fixes - prevent team model reset on model add + return team-only models on /v2/model/info + render team member budget correctly (#12144)

* fix(team_endpoints.py): prevent overwriting current list of team models on new model add

* fix(networking.tsx): fix default proxy base url

* fix(proxy_server.py): include team only models when retrieving all deployments on `/v2/model/info` helper util

ensures team only models are shown to user

* fix(router.py): check model name by team public model name when team id given

Fixes issue where team member could not see team only models when clicking into that team on `Models + Endpoints`

* fix(team_member_view.tsx): fix rendering team member budget, when budget is set

* test: update tests

* test: update unit test

* docs gemini cli x litellm

* docs: index.md

release note cleanup

* docs(index.md): add more hyperlinks to docs

* docs(index.md): add batch api cost tracking to docs

* docs(index.md): update docs

* VertexAI Anthropic - streaming cost tracking w/ prompt caching fixes (#12188)

* fix(rebuild-usage-object---ensure-cache_tokens-is-set): Ensures cache tokens is correctly set

Fixes https://github.com/BerriAI/litellm/issues/12149

* test(test_stream_chunk_builder_utils.py): add unit test to ensure cached tokens is part of stream chunk builder

Ensures standardized values are used

* Fix rendering ui on non-root images (#12226)

* fix(proxy_server.py): only rewrite server_root_path if path set

Fixes UI rendering issue on non-root images

* docs(custom_root_ui.md): clarify custom root path doesn't work on non-root images

* build(pyproject.toml): version rc2

* fix(streaming_handler.py): store finish reason, even if is_finished is false - allows storing early gemini finish reasons (#12250)

Fixes https://github.com/BerriAI/litellm/issues/12249

---------

Co-authored-by: Ishaan Jaff <ishaanjaffer0324@gmail.com>
This commit is contained in:
Krish Dholakia 2025-07-02 20:39:38 -07:00 • committed by GitHub
parent 27849359a9
commit de4f32762a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 2299 additions and 863 deletions

View file

@ -1358,6 +1358,7 @@ jobs:
# - run: python ./tests/documentation_tests/test_general_setting_keys.py
- run: python ./tests/code_coverage_tests/check_licenses.py
- run: python ./tests/code_coverage_tests/router_code_coverage.py
- run: python ./tests/code_coverage_tests/test_proxy_types_import.py
- run: python ./tests/code_coverage_tests/callback_manager_test.py
- run: python ./tests/code_coverage_tests/recursive_detector.py
- run: python ./tests/code_coverage_tests/test_router_strategy_async.py

View file

@ -255,6 +255,198 @@ curl -L -X GET 'http://localhost:4000/user/daily/activity?start_date=2025-03-20&
See our [Swagger API](https://litellm-api.up.railway.app/#/Budget%20%26%20Spend%20Tracking/get_user_daily_activity_user_daily_activity_get) for more details on the `/user/daily/activity` endpoint
## Custom Tags
Requirements:
- Virtual Keys & a database should be set up, see [virtual keys](https://docs.litellm.ai/docs/proxy/virtual_keys)
**Note:** By default, LiteLLM will track `User-Agent` as a custom tag for cost tracking. This enables viewing usage for tools like Claude Code, Gemini CLI, etc.
<Image img={require('../../img/claude_cli_tag_usage.png')} />
### Client-side spend tag
<Tabs>
<TabItem value="key" label="Set on Key">
```bash
curl -L -X POST 'http://0.0.0.0:4000/key/generate' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"metadata": {
"tags": ["tag1", "tag2", "tag3"]
}
}
'
```
</TabItem>
<TabItem value="team" label="Set on Team">
```bash
curl -L -X POST 'http://0.0.0.0:4000/team/new' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"metadata": {
"tags": ["tag1", "tag2", "tag3"]
}
}
'
```
</TabItem>
<TabItem value="openai" label="OpenAI Python v1.0.0+">
Set `extra_body={"metadata": { }}` to `metadata` you want to pass
```python
import openai
client = openai.OpenAI(
api_key="anything",
base_url="http://0.0.0.0:4000"
)
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages = [
{
"role": "user",
"content": "this is a test request, write a short poem"
}
],
extra_body={
"metadata": {
"tags": ["model-anthropic-claude-v2.1", "app-ishaan-prod"] # 👈 Key Change
}
}
)
print(response)
```
</TabItem>
<TabItem value="openai js" label="OpenAI JS">
```js
const openai = require('openai');
async function runOpenAI() {
const client = new openai.OpenAI({
apiKey: 'sk-1234',
baseURL: 'http://0.0.0.0:4000'
});
try {
const response = await client.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [
{
role: 'user',
content: "this is a test request, write a short poem"
},
],
metadata: {
tags: ["model-anthropic-claude-v2.1", "app-ishaan-prod"] // 👈 Key Change
}
});
console.log(response);
} catch (error) {
console.log("got this exception from server");
console.error(error);
}
}
// Call the asynchronous function
runOpenAI();
```
</TabItem>
<TabItem value="Curl" label="Curl Request">
Pass `metadata` as part of the request body
```shell
curl --location 'http://0.0.0.0:4000/chat/completions' \
--header 'Content-Type: application/json' \
--data '{
"model": "gpt-3.5-turbo",
"messages": [
{
"role": "user",
"content": "what llm are you"
}
],
"metadata": {"tags": ["model-anthropic-claude-v2.1", "app-ishaan-prod"]}
}'
```
</TabItem>
<TabItem value="langchain" label="Langchain">
```python
from langchain.chat_models import ChatOpenAI
from langchain.prompts.chat import (
ChatPromptTemplate,
HumanMessagePromptTemplate,
SystemMessagePromptTemplate,
)
from langchain.schema import HumanMessage, SystemMessage
chat = ChatOpenAI(
openai_api_base="http://0.0.0.0:4000",
model = "gpt-3.5-turbo",
temperature=0.1,
extra_body={
"metadata": {
"tags": ["model-anthropic-claude-v2.1", "app-ishaan-prod"]
}
}
)
messages = [
SystemMessage(
content="You are a helpful assistant that im using to make a test request to."
),
HumanMessage(
content="test from litellm. tell me why it's amazing in 1 sentence"
),
]
response = chat(messages)
print(response)
```
</TabItem>
</Tabs>
### Add custom headers to spend tracking
You can add custom headers to the request to track spend and usage.
```yaml
litellm_settings:
extra_spend_tag_headers:
- "x-custom-header"
```
### Disable user-agent tracking
You can disable user-agent tracking by setting `litellm_settings.disable_user_agent_tracking` to `true`.
```yaml
litellm_settings:
disable_user_agent_tracking: true
```
## ✨ (Enterprise) Generate Spend Reports
Use this to charge other teams, customers, users
@ -617,11 +809,5 @@ Logging specific key,value pairs in spend logs metadata is an enterprise feature
:::
## ✨ Custom Tags
:::info
Tracking spend with Custom tags is an enterprise feature. [See here](./enterprise.md#tracking-spend-for-custom-tags)
:::

View file

@ -12,6 +12,9 @@ Requires v1.72.3 or higher.
:::
Limitations:
- This does not work in [litellm non-root](./deploy#non-root---without-internet-connection) images, as it requires write access to the UI files.
## Usage
### 1. Set `SERVER_ROOT_PATH` in your .env

View file

@ -29,7 +29,6 @@ Features:
- ✅ [Team Based Logging](./team_logging.md) - Allow each team to use their own Langfuse Project / custom callbacks
- ✅ [Disable Logging for a Team](./team_logging.md#disable-logging-for-a-team) - Switch off all logging for a team/project (GDPR Compliance)
- **Spend Tracking & Data Exports**
- ✅ [Tracking Spend for Custom Tags](#tracking-spend-for-custom-tags)
- ✅ [Set USD Budgets Spend for Custom Tags](./provider_budget_routing#-tag-budgets)
- ✅ [Set Model budgets for Virtual Keys](./users#-virtual-key-model-specific)
- ✅ [Exporting LLM Logs to GCS Bucket, Azure Blob Storage](./proxy/bucket#🪣-logging-gcs-s3-buckets)
@ -332,174 +331,6 @@ curl --location 'http://0.0.0.0:4000/embeddings' \
## Spend Tracking
### Custom Tags
Requirements:
- Virtual Keys & a database should be set up, see [virtual keys](https://docs.litellm.ai/docs/proxy/virtual_keys)
#### Usage - /chat/completions requests with request tags
<Tabs>
<TabItem value="key" label="Set on Key">
```bash
curl -L -X POST 'http://0.0.0.0:4000/key/generate' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"metadata": {
"tags": ["tag1", "tag2", "tag3"]
}
}
'
```
</TabItem>
<TabItem value="team" label="Set on Team">
```bash
curl -L -X POST 'http://0.0.0.0:4000/team/new' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-d '{
"metadata": {
"tags": ["tag1", "tag2", "tag3"]
}
}
'
```
</TabItem>
<TabItem value="openai" label="OpenAI Python v1.0.0+">
Set `extra_body={"metadata": { }}` to `metadata` you want to pass
```python
import openai
client = openai.OpenAI(
api_key="anything",
base_url="http://0.0.0.0:4000"
)
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages = [
{
"role": "user",
"content": "this is a test request, write a short poem"
}
],
extra_body={
"metadata": {
"tags": ["model-anthropic-claude-v2.1", "app-ishaan-prod"] # 👈 Key Change
}
}
)
print(response)
```
</TabItem>
<TabItem value="openai js" label="OpenAI JS">
```js
const openai = require('openai');
async function runOpenAI() {
const client = new openai.OpenAI({
apiKey: 'sk-1234',
baseURL: 'http://0.0.0.0:4000'
});
try {
const response = await client.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [
{
role: 'user',
content: "this is a test request, write a short poem"
},
],
metadata: {
tags: ["model-anthropic-claude-v2.1", "app-ishaan-prod"] // 👈 Key Change
}
});
console.log(response);
} catch (error) {
console.log("got this exception from server");
console.error(error);
}
}
// Call the asynchronous function
runOpenAI();
```
</TabItem>
<TabItem value="Curl" label="Curl Request">
Pass `metadata` as part of the request body
```shell
curl --location 'http://0.0.0.0:4000/chat/completions' \
--header 'Content-Type: application/json' \
--data '{
"model": "gpt-3.5-turbo",
"messages": [
{
"role": "user",
"content": "what llm are you"
}
],
"metadata": {"tags": ["model-anthropic-claude-v2.1", "app-ishaan-prod"]}
}'
```
</TabItem>
<TabItem value="langchain" label="Langchain">
```python
from langchain.chat_models import ChatOpenAI
from langchain.prompts.chat import (
ChatPromptTemplate,
HumanMessagePromptTemplate,
SystemMessagePromptTemplate,
)
from langchain.schema import HumanMessage, SystemMessage
chat = ChatOpenAI(
openai_api_base="http://0.0.0.0:4000",
model = "gpt-3.5-turbo",
temperature=0.1,
extra_body={
"metadata": {
"tags": ["model-anthropic-claude-v2.1", "app-ishaan-prod"]
}
}
)
messages = [
SystemMessage(
content="You are a helpful assistant that im using to make a test request to."
),
HumanMessage(
content="test from litellm. tell me why it's amazing in 1 sentence"
),
]
response = chat(messages)
print(response)
```
</TabItem>
</Tabs>
#### Viewing Spend per tag
#### `/spend/tags` Request Format

View file

@ -124,7 +124,6 @@ Expected response on failure:
```
</TabItem>
<TabItem label="Successful Call" value="allowed">
```shell

View file

@ -5,7 +5,7 @@ This tutorial shows you how to integrate the Gemini CLI with LiteLLM Proxy, allo
:::info
This integration is supported from LiteLLMv1.73.3-nightly and above.
This integration is supported from LiteLLM v1.73.3-nightly and above.
:::
@ -13,6 +13,19 @@ This integration is supported from LiteLLMv1.73.3-nightly and above.
<iframe width="840" height="500" src="https://www.loom.com/embed/d5dadd811ae64c70b29a16ecd558d4ba" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
## Benefits of using gemini-cli with LiteLLM
When you use gemini-cli with LiteLLM you get the following benefits:
**Developer Benefits:**
- Universal Model Access: Use any LiteLLM supported model (Anthropic, OpenAI, Vertex AI, Bedrock, etc.) through the gemini-cli interface.
- Higher Rate Limits & Reliability: Load balance across multiple models and providers to avoid hitting individual provider limits, with fallbacks to ensure you get responses even if one provider fails.
**Proxy Admin Benefits:**
- Centralized Management: Control access to all models through a single LiteLLM proxy instance without giving your developers API Keys to each provider.
- Budget Controls: Set spending limits and track costs across all gemini-cli usage.
## Prerequisites
@ -63,6 +76,99 @@ The CLI will now use LiteLLM Proxy as the backend, giving you access to LiteLLM'
- Cost tracking
- Model routing and fallbacks
## Advanced
### Use Anthropic, OpenAI, Bedrock, etc. models on gemini-cli
In order to use non-gemini models on gemini-cli, you need to set a `model_group_alias` in the LiteLLM Proxy config. This tells LiteLLM that requests with model = `gemini-2.5-pro` should be routed to your desired model from any provider.
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
<Tabs>
<TabItem value="anthropic" label="Anthropic">
Route `gemini-2.5-pro` requests to Claude Sonnet:
```yaml showLineNumbers title="proxy_config.yaml"
model_list:
- model_name: claude-sonnet-4-20250514
litellm_params:
model: anthropic/claude-3-5-sonnet-20241022
api_key: os.environ/ANTHROPIC_API_KEY
router_settings:
model_group_alias: {"gemini-2.5-pro": "claude-sonnet-4-20250514"}
```
</TabItem>
<TabItem value="openai" label="OpenAI">
Route `gemini-2.5-pro` requests to GPT-4o:
```yaml showLineNumbers title="proxy_config.yaml"
model_list:
- model_name: gpt-4o-model
litellm_params:
model: gpt-4o
api_key: os.environ/OPENAI_API_KEY
router_settings:
model_group_alias: {"gemini-2.5-pro": "gpt-4o-model"}
```
</TabItem>
<TabItem value="bedrock" label="Bedrock">
Route `gemini-2.5-pro` requests to Claude on Bedrock:
```yaml showLineNumbers title="proxy_config.yaml"
model_list:
- model_name: bedrock-claude
litellm_params:
model: bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0
aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID
aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY
aws_region_name: us-east-1
router_settings:
model_group_alias: {"gemini-2.5-pro": "bedrock-claude"}
```
</TabItem>
<TabItem value="multi-provider" label="Multi-Provider Load Balancing">
All deployments with model_name=`anthropic-claude` will be load balanced. In this example we load balance between Anthropic and Bedrock.
```yaml showLineNumbers title="proxy_config.yaml"
model_list:
- model_name: anthropic-claude
litellm_params:
model: anthropic/claude-3-5-sonnet-20241022
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: anthropic-claude
litellm_params:
model: bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0
aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID
aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY
aws_region_name: us-east-1
router_settings:
model_group_alias: {"gemini-2.5-pro": "anthropic-claude"}
```
</TabItem>
</Tabs>
With this configuration, when you use `gemini-2.5-pro` in the CLI, LiteLLM will automatically route your requests to the configured provider(s) with load balancing and fallbacks.
## Troubleshooting
If you encounter issues:

View file

@ -135,3 +135,18 @@ On the models dropdown select `thinking-anthropic-claude-3-7-sonnet`
## Additional Resources
- Running LiteLLM and Open WebUI on Windows Localhost: A Comprehensive Guide [https://www.tanyongsheng.com/note/running-litellm-and-openwebui-on-windows-localhost-a-comprehensive-guide/](https://www.tanyongsheng.com/note/running-litellm-and-openwebui-on-windows-localhost-a-comprehensive-guide/)
## Add Custom Headers to Spend Tracking
You can add custom headers to the request to track spend and usage.
```yaml
litellm_settings:
extra_spend_tag_headers:
- "x-custom-header"
```
You can add custom headers to the request to track spend and usage.
<Image img={require('../../img/custom_tag_headers.png')} />

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

View file

@ -0,0 +1,277 @@
---
title: "[PRE-RELEASE] v1.73.6-stable"
slug: "v1-73-6-stable"
date: 2025-06-28T10:00:00
authors:
- name: Krrish Dholakia
title: CEO, LiteLLM
url: https://www.linkedin.com/in/krish-d/
image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg
- name: Ishaan Jaffer
title: CTO, LiteLLM
url: https://www.linkedin.com/in/reffajnaahsi/
image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg
hide_table_of_contents: false
---
import Image from '@theme/IdealImage';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
:::warning
## Known Issues
The `non-root` docker image has a known issue around the UI not loading. If you use the `non-root` docker image we recommend waiting before upgrading to this version. We will post a patch fix for this.
:::
## Deploy this version
<Tabs>
<TabItem value="docker" label="Docker">
``` showLineNumbers title="docker run litellm"
docker run \
-e STORE_MODEL_IN_DB=True \
-p 4000:4000 \
ghcr.io/berriai/litellm:v1.73.6.rc.1
```
</TabItem>
<TabItem value="pip" label="Pip">
The pip package is not yet available.
</TabItem>
</Tabs>
---
## Key Highlights
### Claude on gemini-cli
<Image img={require('../../img/release_notes/gemini_cli.png')} />
<br/>
This release brings support for using gemini-cli with LiteLLM.
You can use claude-sonnet-4, gemini-2.5-flash (Vertex AI & Google AI Studio), gpt-4.1 and any LiteLLM supported model on gemini-cli.
When you use gemini-cli with LiteLLM you get the following benefits:
**Developer Benefits:**
- Universal Model Access: Use any LiteLLM supported model (Anthropic, OpenAI, Vertex AI, Bedrock, etc.) through the gemini-cli interface.
- Higher Rate Limits & Reliability: Load balance across multiple models and providers to avoid hitting individual provider limits, with fallbacks to ensure you get responses even if one provider fails.
**Proxy Admin Benefits:**
- Centralized Management: Control access to all models through a single LiteLLM proxy instance without giving your developers API Keys to each provider.
- Budget Controls: Set spending limits and track costs across all gemini-cli usage.
[Get Started](../../docs/tutorials/litellm_gemini_cli)
<br/>
### Batch API Cost Tracking
<Image img={require('../../img/release_notes/batch_api_cost_tracking.jpg')}/>
<br/>
v1.73.6 brings cost tracking for [LiteLLM Managed Batch API](../../docs/proxy/managed_batches) calls to LiteLLM. Previously, this was not being done for Batch API calls using LiteLLM Managed Files. Now, LiteLLM will store the status of each batch call in the DB and poll incomplete batch jobs in the background, emitting a spend log for cost tracking once the batch is complete.
There is no new flag / change needed on your end. Over the next few weeks we hope to extend this to cover batch cost tracking for the Anthropic passthrough as well.
[Get Started](../../docs/proxy/managed_batches)
---
## New Models / Updated Models
### Pricing / Context Window Updates
| Provider | Model | Context Window | Input ($/1M tokens) | Output ($/1M tokens) | Type |
| ----------- | -------------------------------------- | -------------- | ------------------- | -------------------- | ---- |
| Azure OpenAI | `azure/o3-pro` | 200k | $20.00 | $80.00 | New |
| OpenRouter | `openrouter/mistralai/mistral-small-3.2-24b-instruct` | 32k | $0.1 | $0.3 | New |
| OpenAI | `o3-deep-research` | 200k | $10.00 | $40.00 | New |
| OpenAI | `o3-deep-research-2025-06-26` | 200k | $10.00 | $40.00 | New |
| OpenAI | `o4-mini-deep-research` | 200k | $2.00 | $8.00 | New |
| OpenAI | `o4-mini-deep-research-2025-06-26` | 200k | $2.00 | $8.00 | New |
| Deepseek | `deepseek-r1` | 65k | $0.55 | $2.19 | New |
| Deepseek | `deepseek-v3` | 65k | $0.27 | $0.07 | New |
### Updated Models
#### Bugs
- **[Sambanova](../../docs/providers/sambanova)**
- Handle float timestamps - [PR](https://github.com/BerriAI/litellm/pull/11971) s/o [@neubig](https://github.com/neubig)
- **[Azure](../../docs/providers/azure)**
- support Azure Authentication method (azure ad token, api keys) on Responses API - [PR](https://github.com/BerriAI/litellm/pull/11941) s/o [@hsuyuming](https://github.com/hsuyuming)
- Map ‘image_url’ str as nested dict - [PR](https://github.com/BerriAI/litellm/pull/12075) s/o [@davis-featherstone](https://github.com/davis-featherstone)
- **[Watsonx](../../docs/providers/watsonx)**
- Set ‘model’ field to None when model is part of a custom deployment - fixes error raised by WatsonX in those cases - [PR](https://github.com/BerriAI/litellm/pull/11854) s/o [@cbjuan](https://github.com/cbjuan)
- **[Perplexity](../../docs/providers/perplexity)**
- Support web_search_options - [PR](https://github.com/BerriAI/litellm/pull/11983)
- Support citation token and search queries cost calculation - [PR](https://github.com/BerriAI/litellm/pull/11938)
- **[Anthropic](../../docs/providers/anthropic)**
- Null value in usage block handling - [PR](https://github.com/BerriAI/litellm/pull/12068)
- **Gemini ([Google AI Studio](../../docs/providers/gemini) + [VertexAI](../../docs/providers/vertex))**
- Only use accepted format values (enum and datetime) - else gemini raises errors - [PR](https://github.com/BerriAI/litellm/pull/11989)
- Cache tools if passed alongside cached content (else gemini raises an error) - [PR](https://github.com/BerriAI/litellm/pull/11989)
- Json schema translation improvement: Fix unpack_def handling of nested $ref inside anyof items - [PR](https://github.com/BerriAI/litellm/pull/11964)
- **[Mistral](../../docs/providers/mistral)**
- Fix thinking prompt to match hugging face recommendation - [PR](https://github.com/BerriAI/litellm/pull/12007)
- Add `supports_response_schema: true` for all mistral models except codestral-mamba - [PR](https://github.com/BerriAI/litellm/pull/12024)
- **[Ollama](../../docs/providers/ollama)**
- Fix unnecessary await on embedding calls - [PR](https://github.com/BerriAI/litellm/pull/12024)
#### Features
- **[Azure OpenAI](../../docs/providers/azure)**
- Check if o-series model supports reasoning effort (enables drop_params to work for o1 models)
- Assistant + tool use cost tracking - [PR](https://github.com/BerriAI/litellm/pull/12045)
- **[Nvidia Nim](../../docs/providers/nvidia_nim)**
- Add ‘response_format’ param support - [PR](https://github.com/BerriAI/litellm/pull/12003) @shagunb-acn 
- **[ElevenLabs](../../docs/providers/elevenlabs)**
- New STT provider - [PR](https://github.com/BerriAI/litellm/pull/12119)
---
## LLM API Endpoints
#### Features
- [**/mcp**](../../docs/mcp)
- Send appropriate auth string value to `/tool/call` endpoint with `x-mcp-auth` - [PR](https://github.com/BerriAI/litellm/pull/11968) s/o [@wagnerjt](https://github.com/wagnerjt)
- [**/v1/messages**](../../docs/anthropic_unified)
- [Custom LLM](../../docs/providers/custom_llm_server#anthropic-v1messages) support - [PR](https://github.com/BerriAI/litellm/pull/12016)
- [**/chat/completions**](../../docs/completion/input)
- Azure Responses API via chat completion support - [PR](https://github.com/BerriAI/litellm/pull/12016)
- [**/responses**](../../docs/response_api)
- Add reasoning content support for non-openai providers - [PR](https://github.com/BerriAI/litellm/pull/12055)
- **[NEW] /generateContent**
- New endpoints for gemini cli support - [PR](https://github.com/BerriAI/litellm/pull/12040)
- Support calling Google AI Studio / VertexAI Gemini models in their native format - [PR](https://github.com/BerriAI/litellm/pull/12046)
- Add logging + cost tracking for stream + non-stream vertex/google ai studio routes - [PR](https://github.com/BerriAI/litellm/pull/12058)
- Add Bridge from generateContent to /chat/completions - [PR](https://github.com/BerriAI/litellm/pull/12081)
- [**/batches**](../../docs/batches)
- Filter deployments to only those where managed file was written to - [PR](https://github.com/BerriAI/litellm/pull/12048)
- Save all model / file id mappings in db (previously it was just the first one) - enables ‘true’ loadbalancing - [PR](https://github.com/BerriAI/litellm/pull/12048)
- Support List Batches with target model name specified - [PR](https://github.com/BerriAI/litellm/pull/12049)
---
## Spend Tracking / Budget Improvements
#### Features
- [**Passthrough**](../../docs/pass_through)
- [Bedrock](../../docs/pass_through/bedrock) - cost tracking (`/invoke` + `/converse` routes) on streaming + non-streaming - [PR](https://github.com/BerriAI/litellm/pull/12123)
- [VertexAI](../../docs/pass_through/vertex_ai) - anthropic cost calculation support - [PR](https://github.com/BerriAI/litellm/pull/11992)
- [**Batches**](../../docs/batches)
- Background job for cost tracking LiteLLM Managed batches - [PR](https://github.com/BerriAI/litellm/pull/12125)
---
## Management Endpoints / UI
#### Bugs
- **General UI**
- Fix today selector date mutation in dashboard components - [PR](https://github.com/BerriAI/litellm/pull/12042)
- **Usage**
- Aggregate usage data across all pages of paginated endpoint - [PR](https://github.com/BerriAI/litellm/pull/12033)
- **Teams**
- De-duplicate models in team settings dropdown - [PR](https://github.com/BerriAI/litellm/pull/12074)
- **Models**
- Preserve public model name when selecting ‘test connect’ with azure model (previously would reset) - [PR](https://github.com/BerriAI/litellm/pull/11713)
- **Invitation Links**
- Ensure Invite links email contain the correct invite id when using tf provider - [PR](https://github.com/BerriAI/litellm/pull/12130)
#### Features
- **Models**
- Add ‘last success’ column to health check table - [PR](https://github.com/BerriAI/litellm/pull/11903)
- **MCP**
- New UI component to support auth types: api key, bearer token, basic auth - [PR](https://github.com/BerriAI/litellm/pull/11968) s/o [@wagnerjt](https://github.com/wagnerjt)
- Ensure internal users can access /mcp and /mcp/ routes - [PR](https://github.com/BerriAI/litellm/pull/12106)
- **SCIM**
- Ensure default_internal_user_params are applied for new users - [PR](https://github.com/BerriAI/litellm/pull/12015)
- **Team**
- Support default key expiry for team member keys - [PR](https://github.com/BerriAI/litellm/pull/12023)
- Expand team member add check to cover user email - [PR](https://github.com/BerriAI/litellm/pull/12082)
- **UI**
- Restrict UI access by SSO group - [PR](https://github.com/BerriAI/litellm/pull/12023)
- **Keys**
- Add new new_key param for regenerating key - [PR](https://github.com/BerriAI/litellm/pull/12087)
- **Test Keys**
- New ‘get code’ button for getting runnable python code snippet based on ui configuration - [PR](https://github.com/BerriAI/litellm/pull/11629)
---
## Logging / Guardrail Integrations
#### Bugs
- **Braintrust**
- Adds model to metadata to enable braintrust cost estimation - [PR](https://github.com/BerriAI/litellm/pull/12022)
#### Features
- **Callbacks**
- (Enterprise) - disable logging callbacks in request headers - [PR](https://github.com/BerriAI/litellm/pull/11985)
- Add List Callbacks API Endpoint - [PR](https://github.com/BerriAI/litellm/pull/11987)
- **Bedrock Guardrail**
- Don't raise exception on intervene action - [PR](https://github.com/BerriAI/litellm/pull/11875)
- Ensure PII Masking is applied on response streaming or non streaming content when using post call - [PR](https://github.com/BerriAI/litellm/pull/12086)
- **[NEW] Palo Alto Networks Prisma AIRS Guardrail**
- [PR](https://github.com/BerriAI/litellm/pull/12116)
- **ElasticSearch**
- New Elasticsearch Logging Tutorial - [PR](https://github.com/BerriAI/litellm/pull/11761)
- **Message Redaction**
- Preserve usage / model information for Embedding redaction - [PR](https://github.com/BerriAI/litellm/pull/12088)
---
## Performance / Loadbalancing / Reliability improvements
#### Bugs
- **Team-only models**
- Filter team-only models from routing logic for non-team calls
- **Context Window Exceeded error**
- Catch anthropic exceptions - [PR](https://github.com/BerriAI/litellm/pull/12113)
#### Features
- **Router**
- allow using dynamic cooldown time for a specific deployment - [PR](https://github.com/BerriAI/litellm/pull/12037)
- handle cooldown_time = 0 for deployments - [PR](https://github.com/BerriAI/litellm/pull/12108)
- **Redis**
- Add better debugging to see what variables are set - [PR](https://github.com/BerriAI/litellm/pull/12073)
---
## General Proxy Improvements
#### Bugs
- **aiohttp**
- Check HTTP_PROXY vars in networking requests
- Allow using HTTP_ Proxy settings with trust_env
#### Features
- **Docs**
- Add recommended spec - [PR](https://github.com/BerriAI/litellm/pull/11980)
- **Swagger**
- Introduce new environment variable NO_REDOC to opt-out Redoc - [PR](https://github.com/BerriAI/litellm/pull/12092)
---
## New Contributors
* @mukesh-dream11 made their first contribution in https://github.com/BerriAI/litellm/pull/11969
* @cbjuan made their first contribution in https://github.com/BerriAI/litellm/pull/11854
* @ryan-castner made their first contribution in https://github.com/BerriAI/litellm/pull/12055
* @davis-featherstone made their first contribution in https://github.com/BerriAI/litellm/pull/12075
* @Gum-Joe made their first contribution in https://github.com/BerriAI/litellm/pull/12068
* @jroberts2600 made their first contribution in https://github.com/BerriAI/litellm/pull/12116
* @ohmeow made their first contribution in https://github.com/BerriAI/litellm/pull/12022
* @amarrella made their first contribution in https://github.com/BerriAI/litellm/pull/11942
* @zhangyoufu made their first contribution in https://github.com/BerriAI/litellm/pull/12092
* @bougou made their first contribution in https://github.com/BerriAI/litellm/pull/12088
* @codeugar made their first contribution in https://github.com/BerriAI/litellm/pull/11972
* @glgh made their first contribution in https://github.com/BerriAI/litellm/pull/12133
## **[Git Diff](https://github.com/BerriAI/litellm/compare/v1.73.0-stable...v1.73.6.rc-draft)**

Binary file not shown.

Binary file not shown.

View file

@ -1,7 +1,7 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
package = []
[metadata]
lock-version = "2.0"
lock-version = "2.1"
python-versions = ">=3.8.1,<4.0, !=3.9.7"
content-hash = "2cf39473e67ff0615f0a61c9d2ac9f02b38cc08cbb1bdb893d89bee002646623"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "litellm-enterprise"
version = "0.1.9"
version = "0.1.10"
description = "Package for LiteLLM Enterprise features"
authors = ["BerriAI"]
readme = "README.md"
@ -22,7 +22,7 @@ requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.commitizen]
version = "0.1.9"
version = "0.1.10"
version_files = [
"pyproject.toml:version",
"../requirements.txt:litellm-enterprise==",

View file

@ -61,12 +61,8 @@ from litellm.constants import (
DEFAULT_ALLOWED_FAILS,
)
from litellm.types.guardrails import GuardrailItem
from litellm.proxy._types import (
KeyManagementSystem,
KeyManagementSettings,
LiteLLM_UpperboundKeyGenerateParams,
)
from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams
from litellm.types.secret_managers.main import KeyManagementSystem, KeyManagementSettings
from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams, LiteLLM_UpperboundKeyGenerateParams
from litellm.types.utils import StandardKeyGenerationConfig, LlmProviders
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.logging_callback_manager import LoggingCallbackManager
@ -76,6 +72,7 @@ import dotenv
litellm_mode = os.getenv("LITELLM_MODE", "DEV") # "PRODUCTION", "DEV"
if litellm_mode == "DEV":
dotenv.load_dotenv()
##################################################
if set_verbose == True:
_turn_on_debug()
@ -221,6 +218,7 @@ disable_streaming_logging: bool = False
disable_token_counter: bool = False
disable_add_transform_inline_image_block: bool = False
disable_add_user_agent_to_request_tags: bool = False
extra_spend_tag_headers: Optional[List[str]] = None
in_memory_llm_clients_cache: LLMClientCache = LLMClientCache()
safe_memory_mode: bool = False
enable_azure_ad_token_refresh: Optional[bool] = False
@ -323,9 +321,11 @@ priority_reservation: Optional[Dict[str, float]] = None
use_aiohttp_transport: bool = (
True # Older variable, aiohttp is now the default. use disable_aiohttp_transport instead.
)
aiohttp_trust_env: bool = False # set to true to use HTTP_ Proxy settings
aiohttp_trust_env: bool = False # set to true to use HTTP_ Proxy settings
disable_aiohttp_transport: bool = False # Set this to true to use httpx instead
disable_aiohttp_trust_env: bool = False # When False, aiohttp will respect HTTP(S)_PROXY env vars
disable_aiohttp_trust_env: bool = (
False # When False, aiohttp will respect HTTP(S)_PROXY env vars
)
force_ipv4: bool = (
False # when True, litellm will force ipv4 for all LLM requests. Some users have seen httpx ConnectionError when using ipv6.
)
@ -1157,6 +1157,7 @@ from .fine_tuning.main import *
from .files.main import *
from .scheduler import *
from .cost_calculator import response_cost_calculator, cost_per_token
### ADAPTERS ###
from .types.adapter import AdapterItem
import litellm.anthropic_interface as anthropic

View file

@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Any, Optional, Union
import litellm
from litellm._logging import verbose_logger
from litellm.proxy._types import UserAPIKeyAuth
from .integrations.custom_logger import CustomLogger
from .integrations.datadog.datadog import DataDogLogger
@ -15,11 +14,14 @@ from .types.services import ServiceLoggerPayload, ServiceTypes
if TYPE_CHECKING:
from opentelemetry.trace import Span as _Span
from litellm.proxy._types import UserAPIKeyAuth
Span = Union[_Span, Any]
OTELClass = OpenTelemetry
else:
Span = Any
OTELClass = Any
UserAPIKeyAuth = Any
class ServiceLogging(CustomLogger):

View file

@ -26,50 +26,53 @@ class GoogleGenAIStreamWrapper(AdapterCompletionStreamWrapper):
Wrapper for streaming Google GenAI generate_content responses.
Transforms OpenAI streaming chunks to Google GenAI format.
"""
sent_first_chunk: bool = False
# State tracking for accumulating partial tool calls
accumulated_tool_calls: Dict[str, Dict[str, Any]]
def __init__(self, completion_stream: Any):
super().__init__(completion_stream)
self.sent_first_chunk = False
self.accumulated_tool_calls = {}
def __next__(self):
try:
for chunk in self.completion_stream:
if chunk == "None" or chunk is None:
continue
# Transform OpenAI streaming chunk to Google GenAI format
transformed_chunk = GoogleGenAIAdapter().translate_streaming_completion_to_generate_content(chunk, self)
transformed_chunk = GoogleGenAIAdapter().translate_streaming_completion_to_generate_content(
chunk, self
)
if transformed_chunk: # Only return non-empty chunks
return transformed_chunk
raise StopIteration
except StopIteration:
raise StopIteration
except Exception:
raise StopIteration
async def __anext__(self):
try:
async for chunk in self.completion_stream:
if chunk == "None" or chunk is None:
continue
# Transform OpenAI streaming chunk to Google GenAI format
transformed_chunk = GoogleGenAIAdapter().translate_streaming_completion_to_generate_content(chunk, self)
# Transform OpenAI streaming chunk to Google GenAI format
transformed_chunk = GoogleGenAIAdapter().translate_streaming_completion_to_generate_content(
chunk, self
)
if transformed_chunk: # Only return non-empty chunks
return transformed_chunk
raise StopAsyncIteration
except StopAsyncIteration:
raise StopAsyncIteration
except Exception:
raise StopAsyncIteration
def google_genai_sse_wrapper(self) -> Iterator[bytes]:
"""
Convert Google GenAI streaming chunks to Server-Sent Events format.
@ -80,7 +83,7 @@ class GoogleGenAIStreamWrapper(AdapterCompletionStreamWrapper):
yield payload.encode()
else:
yield chunk
async def async_google_genai_sse_wrapper(self) -> AsyncIterator[bytes]:
"""
Async version of google_genai_sse_wrapper.
@ -95,40 +98,39 @@ class GoogleGenAIStreamWrapper(AdapterCompletionStreamWrapper):
class GoogleGenAIAdapter:
"""Adapter for transforming Google GenAI generate_content requests to/from litellm.completion format"""
def __init__(self) -> None:
pass
def translate_generate_content_to_completion(
self,
model: str,
contents: Union[List[Dict[str, Any]], Dict[str, Any]],
config: Optional[Dict[str, Any]] = None,
**kwargs
**kwargs,
) -> ChatCompletionRequest:
"""
Transform generate_content request to litellm completion format
Args:
model: The model name
contents: Generate content contents (can be list or single dict)
config: Optional config parameters
**kwargs: Additional parameters
Returns:
ChatCompletionRequest in OpenAI format
"""
# Normalize contents to list format
if isinstance(contents, dict):
contents_list = [contents]
else:
contents_list = contents
# Transform contents to OpenAI messages format
messages = self._transform_contents_to_messages(contents_list)
# Create base request
completion_request: ChatCompletionRequest = ChatCompletionRequest(
model=model,
@ -146,7 +148,7 @@ class GoogleGenAIAdapter:
# - tools
# - tool_choice
#########################################################
# Add config parameters if provided
if config:
# Map common Google GenAI config parameters to OpenAI equivalents
@ -161,89 +163,91 @@ class GoogleGenAIAdapter:
pass
if "stopSequences" in config:
completion_request["stop"] = config["stopSequences"]
# Handle tools transformation
if "tools" in kwargs:
tools = kwargs["tools"]
# Check if tools are already in OpenAI format or Google GenAI format
if isinstance(tools, list) and len(tools) > 0:
# Tools are in Google GenAI format, transform them
openai_tools = self._transform_google_genai_tools_to_openai(tools)
if openai_tools:
completion_request["tools"] = openai_tools
# Handle tool_config (tool choice)
if "tool_config" in kwargs:
tool_choice = self._transform_google_genai_tool_config_to_openai(kwargs["tool_config"])
tool_choice = self._transform_google_genai_tool_config_to_openai(
kwargs["tool_config"]
)
if tool_choice:
completion_request["tool_choice"] = tool_choice
return completion_request
def translate_completion_output_params_streaming(
self, completion_stream: Any
) -> Union[AsyncIterator[bytes], None]:
"""Transform streaming completion output to Google GenAI format"""
google_genai_wrapper = GoogleGenAIStreamWrapper(completion_stream=completion_stream)
google_genai_wrapper = GoogleGenAIStreamWrapper(
completion_stream=completion_stream
)
# Return the SSE-wrapped version for proper event formatting
return google_genai_wrapper.async_google_genai_sse_wrapper()
def _transform_google_genai_tools_to_openai(self, tools: List[Dict[str, Any]]) -> List[ChatCompletionToolParam]:
def _transform_google_genai_tools_to_openai(
self, tools: List[Dict[str, Any]]
) -> List[ChatCompletionToolParam]:
"""Transform Google GenAI tools to OpenAI tools format"""
openai_tools: List[Dict[str, Any]] = []
for tool in tools:
if "functionDeclarations" in tool:
for func_decl in tool["functionDeclarations"]:
function_chunk: Dict[str, Any] = {
"name": func_decl.get("name", ""),
}
if "description" in func_decl:
function_chunk["description"] = func_decl["description"]
if "parameters" in func_decl:
function_chunk["parameters"] = func_decl["parameters"]
openai_tool = {
"type": "function",
"function": function_chunk
}
openai_tool = {"type": "function", "function": function_chunk}
openai_tools.append(openai_tool)
# normalize the tool schemas
normalized_tools = [normalize_tool_schema(tool) for tool in openai_tools]
return cast(List[ChatCompletionToolParam], normalized_tools)
def _transform_google_genai_tool_config_to_openai(self, tool_config: Dict[str, Any]) -> Optional[ChatCompletionToolChoiceValues]:
def _transform_google_genai_tool_config_to_openai(
self, tool_config: Dict[str, Any]
) -> Optional[ChatCompletionToolChoiceValues]:
"""Transform Google GenAI tool_config to OpenAI tool_choice"""
function_calling_config = tool_config.get("functionCallingConfig", {})
mode = function_calling_config.get("mode", "AUTO")
mode_mapping = {
"AUTO": "auto",
"ANY": "required",
"NONE": "none"
}
mode_mapping = {"AUTO": "auto", "ANY": "required", "NONE": "none"}
tool_choice = mode_mapping.get(mode, "auto")
return cast(ChatCompletionToolChoiceValues, tool_choice)
def _transform_contents_to_messages(self, contents: List[Dict[str, Any]]) -> List[AllMessageValues]:
def _transform_contents_to_messages(
self, contents: List[Dict[str, Any]]
) -> List[AllMessageValues]:
"""Transform Google GenAI contents to OpenAI messages format"""
messages: List[AllMessageValues] = []
for content in contents:
role = content.get("role", "user")
parts = content.get("parts", [])
if role == "user":
# Handle user messages with potential function responses
combined_text = ""
tool_messages: List[ChatCompletionToolMessage] = []
for part in parts:
if isinstance(part, dict):
if "text" in part:
@ -254,27 +258,26 @@ class GoogleGenAIAdapter:
tool_message = ChatCompletionToolMessage(
role="tool",
tool_call_id=f"call_{func_response.get('name', 'unknown')}",
content=json.dumps(func_response.get("response", {}))
content=json.dumps(func_response.get("response", {})),
)
tool_messages.append(tool_message)
elif isinstance(part, str):
combined_text += part
# Add user message if there's text content
if combined_text:
messages.append(ChatCompletionUserMessage(
role="user",
content=combined_text
))
messages.append(
ChatCompletionUserMessage(role="user", content=combined_text)
)
# Add tool messages
messages.extend(tool_messages)
elif role == "model":
# Handle assistant messages with potential function calls
combined_text = ""
tool_calls: List[ChatCompletionAssistantToolCall] = []
for part in parts:
if isinstance(part, dict):
if "text" in part:
@ -287,28 +290,28 @@ class GoogleGenAIAdapter:
type="function",
function=ChatCompletionToolCallFunctionChunk(
name=func_call.get("name", ""),
arguments=json.dumps(func_call.get("args", {}))
)
arguments=json.dumps(func_call.get("args", {})),
),
)
tool_calls.append(tool_call)
elif isinstance(part, str):
combined_text += part
# Create assistant message
if tool_calls:
assistant_message = ChatCompletionAssistantMessage(
role="assistant",
content=combined_text if combined_text else None,
tool_calls=tool_calls
tool_calls=tool_calls,
)
else:
assistant_message = ChatCompletionAssistantMessage(
role="assistant",
content=combined_text if combined_text else None
content=combined_text if combined_text else None,
)
messages.append(assistant_message)
return messages
def translate_completion_to_generate_content(
@ -316,57 +319,62 @@ class GoogleGenAIAdapter:
) -> Dict[str, Any]:
"""
Transform litellm completion response to Google GenAI generate_content format
Args:
response: ModelResponse from litellm.completion
Returns:
Dict in Google GenAI generate_content response format
"""
# Extract the main response content
choice = response.choices[0] if response.choices else None
if not choice:
raise ValueError("Invalid completion response: no choices found")
# Handle different choice types (Choices vs StreamingChoices)
if isinstance(choice, Choices):
if not choice.message:
raise ValueError("Invalid completion response: no message found in choice")
raise ValueError(
"Invalid completion response: no message found in choice"
)
parts = self._transform_openai_message_to_google_genai_parts(choice.message)
elif isinstance(choice, StreamingChoices):
if not choice.delta:
raise ValueError("Invalid completion response: no delta found in streaming choice")
raise ValueError(
"Invalid completion response: no delta found in streaming choice"
)
parts = self._transform_openai_delta_to_google_genai_parts(choice.delta)
else:
# Fallback for generic choice objects
message_content = getattr(choice, 'message', {}).get('content', '') or getattr(choice, 'delta', {}).get('content', '')
message_content = getattr(choice, "message", {}).get(
"content", ""
) or getattr(choice, "delta", {}).get("content", "")
parts = [{"text": message_content}] if message_content else []
# Create Google GenAI format response
generate_content_response: Dict[str, Any] = {
"candidates": [
{
"content": {
"parts": parts,
"role": "model"
},
"finishReason": self._map_finish_reason(getattr(choice, 'finish_reason', None)),
"content": {"parts": parts, "role": "model"},
"finishReason": self._map_finish_reason(
getattr(choice, "finish_reason", None)
),
"index": 0,
"safetyRatings": []
"safetyRatings": [],
}
],
"usageMetadata": (
self._map_usage(getattr(response, 'usage', None))
if hasattr(response, 'usage') and getattr(response, 'usage', None)
self._map_usage(getattr(response, "usage", None))
if hasattr(response, "usage") and getattr(response, "usage", None)
else {
"promptTokenCount": 0,
"candidatesTokenCount": 0,
"totalTokenCount": 0
"totalTokenCount": 0,
}
)
),
}
# Add text field for convenience (common in Google GenAI responses)
text_content = ""
for part in parts:
@ -374,7 +382,7 @@ class GoogleGenAIAdapter:
text_content += part["text"]
if text_content:
generate_content_response["text"] = text_content
return generate_content_response
def translate_streaming_completion_to_generate_content(
@ -382,62 +390,69 @@ class GoogleGenAIAdapter:
) -> Dict[str, Any]:
"""
Transform streaming litellm completion chunk to Google GenAI generate_content format
Args:
response: Streaming ModelResponse chunk from litellm.completion
wrapper: GoogleGenAIStreamWrapper instance
Returns:
Dict in Google GenAI streaming generate_content response format
"""
# Extract the main response content from streaming chunk
choice = response.choices[0] if response.choices else None
if not choice:
# Return empty chunk if no choices
return {}
# Handle streaming choice
if isinstance(choice, StreamingChoices):
if choice.delta:
parts = self._transform_openai_delta_to_google_genai_parts_with_accumulation(choice.delta, wrapper)
parts = self._transform_openai_delta_to_google_genai_parts_with_accumulation(
choice.delta, wrapper
)
else:
parts = []
finish_reason = getattr(choice, 'finish_reason', None)
finish_reason = getattr(choice, "finish_reason", None)
else:
# Fallback for generic choice objects
message_content = getattr(choice, 'delta', {}).get('content', '')
message_content = getattr(choice, "delta", {}).get("content", "")
parts = [{"text": message_content}] if message_content else []
finish_reason = getattr(choice, 'finish_reason', None)
finish_reason = getattr(choice, "finish_reason", None)
# Only create response chunk if we have parts or it's the final chunk
if not parts and not finish_reason:
return {}
# Create Google GenAI streaming format response
streaming_chunk: Dict[str, Any] = {
"candidates": [
{
"content": {
"parts": parts,
"role": "model"
},
"finishReason": self._map_finish_reason(finish_reason) if finish_reason else None,
"content": {"parts": parts, "role": "model"},
"finishReason": (
self._map_finish_reason(finish_reason)
if finish_reason
else None
),
"index": 0,
"safetyRatings": []
"safetyRatings": [],
}
]
}
# Add usage metadata only in the final chunk (when finish_reason is present)
if finish_reason:
usage_metadata = self._map_usage(getattr(response, 'usage', None)) if hasattr(response, 'usage') and getattr(response, 'usage', None) else {
"promptTokenCount": 0,
"candidatesTokenCount": 0,
"totalTokenCount": 0
}
usage_metadata = (
self._map_usage(getattr(response, "usage", None))
if hasattr(response, "usage") and getattr(response, "usage", None)
else {
"promptTokenCount": 0,
"candidatesTokenCount": 0,
"totalTokenCount": 0,
}
)
streaming_chunk["usageMetadata"] = usage_metadata
# Add text field for convenience (common in Google GenAI responses)
text_content = ""
for part in parts:
@ -445,64 +460,69 @@ class GoogleGenAIAdapter:
text_content += part["text"]
if text_content:
streaming_chunk["text"] = text_content
return streaming_chunk
def _transform_openai_message_to_google_genai_parts(self, message: Any) -> List[Dict[str, Any]]:
def _transform_openai_message_to_google_genai_parts(
self, message: Any
) -> List[Dict[str, Any]]:
"""Transform OpenAI message to Google GenAI parts format"""
parts: List[Dict[str, Any]] = []
# Add text content if present
if hasattr(message, 'content') and message.content:
if hasattr(message, "content") and message.content:
parts.append({"text": message.content})
# Add tool calls if present
if hasattr(message, 'tool_calls') and message.tool_calls:
if hasattr(message, "tool_calls") and message.tool_calls:
for tool_call in message.tool_calls:
if hasattr(tool_call, 'function') and tool_call.function:
if hasattr(tool_call, "function") and tool_call.function:
try:
args = json.loads(tool_call.function.arguments) if tool_call.function.arguments else {}
args = (
json.loads(tool_call.function.arguments)
if tool_call.function.arguments
else {}
)
except json.JSONDecodeError:
args = {}
function_call_part = {
"functionCall": {
"name": tool_call.function.name,
"args": args
}
"functionCall": {"name": tool_call.function.name, "args": args}
}
parts.append(function_call_part)
return parts if parts else [{"text": ""}]
def _transform_openai_delta_to_google_genai_parts(self, delta: Any) -> List[Dict[str, Any]]:
def _transform_openai_delta_to_google_genai_parts(
self, delta: Any
) -> List[Dict[str, Any]]:
"""Transform OpenAI delta to Google GenAI parts format for streaming"""
parts: List[Dict[str, Any]] = []
# Add text content if present
if hasattr(delta, 'content') and delta.content:
if hasattr(delta, "content") and delta.content:
parts.append({"text": delta.content})
# Add tool calls if present (for streaming tool calls)
if hasattr(delta, 'tool_calls') and delta.tool_calls:
if hasattr(delta, "tool_calls") and delta.tool_calls:
for tool_call in delta.tool_calls:
if hasattr(tool_call, 'function') and tool_call.function:
if hasattr(tool_call, "function") and tool_call.function:
# For streaming, we might get partial function arguments
args_str = getattr(tool_call.function, 'arguments', '') or ''
args_str = getattr(tool_call.function, "arguments", "") or ""
try:
args = json.loads(args_str) if args_str else {}
except json.JSONDecodeError:
# For partial JSON in streaming, return as text for now
args = {"partial": args_str}
function_call_part = {
"functionCall": {
"name": getattr(tool_call.function, 'name', '') or '',
"args": args
"name": getattr(tool_call.function, "name", "") or "",
"args": args,
}
}
parts.append(function_call_part)
return parts
def _transform_openai_delta_to_google_genai_parts_with_accumulation(
@ -510,74 +530,84 @@ class GoogleGenAIAdapter:
) -> List[Dict[str, Any]]:
"""Transform OpenAI delta to Google GenAI parts format with tool call accumulation"""
parts: List[Dict[str, Any]] = []
# Add text content if present
if hasattr(delta, 'content') and delta.content:
if hasattr(delta, "content") and delta.content:
parts.append({"text": delta.content})
# Handle tool calls with accumulation for streaming
if hasattr(delta, 'tool_calls') and delta.tool_calls:
if hasattr(delta, "tool_calls") and delta.tool_calls:
for tool_call in delta.tool_calls:
if hasattr(tool_call, 'function') and tool_call.function:
tool_call_id = getattr(tool_call, 'id', '') or 'call_unknown'
function_name = getattr(tool_call.function, 'name', '') or ''
args_str = getattr(tool_call.function, 'arguments', '') or ''
if hasattr(tool_call, "function") and tool_call.function:
tool_call_id = getattr(tool_call, "id", "") or "call_unknown"
function_name = getattr(tool_call.function, "name", "") or ""
args_str = getattr(tool_call.function, "arguments", "") or ""
# Initialize accumulation for this tool call if not exists
if tool_call_id not in wrapper.accumulated_tool_calls:
wrapper.accumulated_tool_calls[tool_call_id] = {
'name': '',
'arguments': '',
'complete': False
"name": "",
"arguments": "",
"complete": False,
}
# Accumulate function name if provided
if function_name:
wrapper.accumulated_tool_calls[tool_call_id]['name'] = function_name
wrapper.accumulated_tool_calls[tool_call_id][
"name"
] = function_name
# Accumulate arguments if provided
if args_str:
wrapper.accumulated_tool_calls[tool_call_id]['arguments'] += args_str
wrapper.accumulated_tool_calls[tool_call_id][
"arguments"
] += args_str
# Try to parse the accumulated arguments as JSON
accumulated_args = wrapper.accumulated_tool_calls[tool_call_id]['arguments']
accumulated_args = wrapper.accumulated_tool_calls[tool_call_id][
"arguments"
]
try:
if accumulated_args:
parsed_args = json.loads(accumulated_args)
# JSON is valid, mark as complete and create function call part
wrapper.accumulated_tool_calls[tool_call_id]['complete'] = True
wrapper.accumulated_tool_calls[tool_call_id][
"complete"
] = True
function_call_part = {
"functionCall": {
"name": wrapper.accumulated_tool_calls[tool_call_id]['name'],
"args": parsed_args
"name": wrapper.accumulated_tool_calls[
tool_call_id
]["name"],
"args": parsed_args,
}
}
parts.append(function_call_part)
# Clean up completed tool call
del wrapper.accumulated_tool_calls[tool_call_id]
except json.JSONDecodeError:
# JSON is still incomplete, continue accumulating
# Don't add to parts yet
pass
return parts
def _map_finish_reason(self, finish_reason: Optional[str]) -> str:
"""Map OpenAI finish reasons to Google GenAI finish reasons"""
if not finish_reason:
return "STOP"
mapping = {
"stop": "STOP",
"length": "MAX_TOKENS",
"length": "MAX_TOKENS",
"content_filter": "SAFETY",
"tool_calls": "STOP",
"function_call": "STOP",
}
return mapping.get(finish_reason, "STOP")
def _map_usage(self, usage: Any) -> Dict[str, int]:
@ -586,4 +616,4 @@ class GoogleGenAIAdapter:
"promptTokenCount": getattr(usage, "prompt_tokens", 0) or 0,
"candidatesTokenCount": getattr(usage, "completion_tokens", 0) or 0,
"totalTokenCount": getattr(usage, "total_tokens", 0) or 0,
}
}

View file

@ -16,7 +16,6 @@ from typing import (
from pydantic import BaseModel
from litellm.caching.caching import DualCache
from litellm.proxy._types import UserAPIKeyAuth
from litellm.types.integrations.argilla import ArgillaItem
from litellm.types.llms.openai import AllMessageValues, ChatCompletionRequest
from litellm.types.utils import (
@ -33,11 +32,13 @@ if TYPE_CHECKING:
from opentelemetry.trace import Span as _Span
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.proxy._types import UserAPIKeyAuth
Span = Union[_Span, Any]
else:
Span = Any
LiteLLMLoggingObj = Any
UserAPIKeyAuth = Any
class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callback#callback-class

View file

@ -116,7 +116,6 @@ from ..integrations.argilla import ArgillaLogger
from ..integrations.arize.arize_phoenix import ArizePhoenixLogger
from ..integrations.athina import AthinaLogger
from ..integrations.azure_storage.azure_storage import AzureBlobStorageLogger
from ..integrations.braintrust_logging import BraintrustLogger
from ..integrations.custom_prompt_management import CustomPromptManagement
from ..integrations.datadog.datadog import DataDogLogger
from ..integrations.datadog.datadog_llm_obs import DataDogLLMObsLogger
@ -144,7 +143,6 @@ from ..integrations.s3 import S3Logger
from ..integrations.s3_v2 import S3Logger as S3V2Logger
from ..integrations.supabase import Supabase
from ..integrations.traceloop import TraceloopLogger
from ..integrations.weights_biases import WeightsBiasesLogger
from .exception_mapping_utils import _get_response_headers
from .initialize_dynamic_callback_params import (
initialize_standard_callback_dynamic_params as _initialize_standard_callback_dynamic_params,
@ -3022,6 +3020,7 @@ def set_callbacks(callback_list, function_id=None): # noqa: PLR0915
elif callback == "s3":
s3Logger = S3Logger()
elif callback == "wandb":
from litellm.integrations.weights_biases import WeightsBiasesLogger
weightsBiasesLogger = WeightsBiasesLogger()
elif callback == "logfire":
logfireLogger = LogfireLogger()
@ -3075,6 +3074,7 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915
_in_memory_loggers.append(_openmeter_logger)
return _openmeter_logger # type: ignore
elif logging_integration == "braintrust":
from litellm.integrations.braintrust_logging import BraintrustLogger
for callback in _in_memory_loggers:
if isinstance(callback, BraintrustLogger):
return callback # type: ignore
@ -3432,6 +3432,7 @@ def get_custom_logger_compatible_class( # noqa: PLR0915
if isinstance(callback, OpenMeterLogger):
return callback
elif logging_integration == "braintrust":
from litellm.integrations.braintrust_logging import BraintrustLogger
for callback in _in_memory_loggers:
if isinstance(callback, BraintrustLogger):
return callback
@ -4023,6 +4024,27 @@ class StandardLoggingPayloadSetup:
user_agent_tags.append("User-Agent: " + user_agent)
return user_agent_tags
@staticmethod
def _get_extra_header_tags(proxy_server_request: dict) -> Optional[List[str]]:
"""
Extract additional header tags for spend tracking based on config.
"""
extra_headers: List[str] = litellm.extra_spend_tag_headers or []
if not extra_headers:
return None
headers = proxy_server_request.get("headers", {})
if not isinstance(headers, dict):
return None
header_tags = []
for header_name in extra_headers:
header_value = headers.get(header_name)
if header_value:
header_tags.append(f"{header_name}: {header_value}")
return header_tags if header_tags else None
@staticmethod
def _get_request_tags(metadata: dict, proxy_server_request: dict) -> List[str]:
request_tags = (
@ -4033,8 +4055,13 @@ class StandardLoggingPayloadSetup:
user_agent_tags = StandardLoggingPayloadSetup._get_user_agent_tags(
proxy_server_request
)
additional_header_tags = StandardLoggingPayloadSetup._get_extra_header_tags(
proxy_server_request
)
if user_agent_tags is not None:
request_tags.extend(user_agent_tags)
if additional_header_tags is not None:
request_tags.extend(additional_header_tags)
return request_tags

View file

@ -248,7 +248,7 @@ class StandardBuiltInToolCostTracking:
)
@staticmethod
def _extract_token_counts(computer_use_usage: Any) -> tuple[Optional[int], Optional[int]]:
def _extract_token_counts(computer_use_usage: Any) -> Tuple[Optional[int], Optional[int]]:
"""Extract and convert token counts safely."""
input_tokens = None
output_tokens = None

View file

@ -107,9 +107,9 @@ class ChunkProcessor:
self, tool_call_chunks: List[Dict[str, Any]]
) -> List[ChatCompletionMessageToolCall]:
tool_calls_list: List[ChatCompletionMessageToolCall] = []
tool_call_map: Dict[
int, Dict[str, Any]
] = {} # Map to store tool calls by index
tool_call_map: Dict[int, Dict[str, Any]] = (
{}
) # Map to store tool calls by index
for chunk in tool_call_chunks:
choices = chunk["choices"]
@ -415,6 +415,8 @@ class ChunkProcessor:
if prompt_tokens_details is not None:
returned_usage.prompt_tokens_details = prompt_tokens_details
# Return a new usage object with the new values
returned_usage = Usage(**returned_usage.model_dump())
return returned_usage

View file

@ -758,6 +758,7 @@ class CustomStreamWrapper:
is_chunk_non_empty = self.is_chunk_non_empty(
completion_obj, model_response, response_obj
)
if (
is_chunk_non_empty
): # cannot set content of an OpenAI Object to be an empty string
@ -1203,6 +1204,7 @@ class CustomStreamWrapper:
if response_obj is None:
return
completion_obj["content"] = response_obj["text"]
self.received_finish_reason = response_obj.get("finish_reason", None)
if response_obj["is_finished"]:
if response_obj["finish_reason"] == "error":
raise Exception(
@ -1210,7 +1212,6 @@ class CustomStreamWrapper:
self.custom_llm_provider, response_obj
)
)
self.received_finish_reason = response_obj["finish_reason"]
if response_obj.get("original_chunk", None) is not None:
if hasattr(response_obj["original_chunk"], "id"):
model_response = self.set_model_id(

View file

@ -153,7 +153,8 @@ class LiteLLMMessagesToCompletionTransformationHandler:
if stream:
transformed_stream = (
ANTHROPIC_ADAPTER.translate_completion_output_params_streaming(
completion_response
completion_response,
model=model,
)
)
if transformed_stream is not None:
@ -239,7 +240,8 @@ class LiteLLMMessagesToCompletionTransformationHandler:
if stream:
transformed_stream = (
ANTHROPIC_ADAPTER.translate_completion_output_params_streaming(
completion_response
completion_response,
model=model,
)
)
if transformed_stream is not None:

View file

@ -2,6 +2,7 @@
## Translates OpenAI call to Anthropic `/v1/messages` format
import json
import traceback
import uuid
from typing import Any, AsyncIterator, Iterator, Optional
from litellm import verbose_logger
@ -16,6 +17,10 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper):
- finish_reason must map exactly to anthropic reason, else anthropic client won't be able to parse it.
"""
def __init__(self, completion_stream: Any, model: str):
super().__init__(completion_stream)
self.model = model
sent_first_chunk: bool = False
sent_content_block_start: bool = False
sent_content_block_finish: bool = False
@ -31,11 +36,11 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper):
return {
"type": "message_start",
"message": {
"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY",
"id": "msg_{}".format(uuid.uuid4()),
"type": "message",
"role": "assistant",
"content": [],
"model": "claude-3-5-sonnet-20240620",
"model": self.model,
"stop_reason": None,
"stop_sequence": None,
"usage": UsageDelta(input_tokens=0, output_tokens=0),
@ -100,11 +105,11 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper):
return {
"type": "message_start",
"message": {
"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY",
"id": "msg_{}".format(uuid.uuid4()),
"type": "message",
"role": "assistant",
"content": [],
"model": "claude-3-5-sonnet-20240620",
"model": self.model,
"stop_reason": None,
"stop_sequence": None,
"usage": UsageDelta(input_tokens=0, output_tokens=0),

View file

@ -96,9 +96,11 @@ class AnthropicAdapter:
)
def translate_completion_output_params_streaming(
self, completion_stream: Any
self, completion_stream: Any, model: str
) -> Union[AsyncIterator[bytes], None]:
anthropic_wrapper = AnthropicStreamWrapper(completion_stream=completion_stream)
anthropic_wrapper = AnthropicStreamWrapper(
completion_stream=completion_stream, model=model
)
# Return the SSE-wrapped version for proper event formatting
return anthropic_wrapper.async_anthropic_sse_wrapper()

View file

@ -38,13 +38,38 @@ class AzureOpenAIO1Config(OpenAIOSeriesConfig):
"top_logprobs",
]
o_series_only_param = []
if supports_reasoning(model):
o_series_only_param.append("reasoning_effort")
o_series_only_param = self._get_o_series_only_params(model)
all_openai_params.extend(o_series_only_param)
return [
param for param in all_openai_params if param not in non_supported_params
]
def _get_o_series_only_params(self, model: str) -> list:
"""
Helper function to get the o-series only params for the model
- reasoning_effort
"""
o_series_only_param = []
#########################################################
# Case 1: If the model is recognized and in litellm model cost map
# then check if it supports reasoning
#########################################################
if model in litellm.model_list_set:
if supports_reasoning(model):
o_series_only_param.append("reasoning_effort")
#########################################################
# Case 2: If the model is not recognized, then we assume it supports reasoning
# This is critical because several users tend to use custom deployment names
# for azure o-series models.
#########################################################
else:
o_series_only_param.append("reasoning_effort")
return o_series_only_param
def should_fake_stream(
self,

View file

@ -115,6 +115,7 @@ class BaseAnthropicMessagesConfig(ABC):
self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers]
) -> "BaseLLMException":
from litellm.llms.base_llm.chat.transformation import BaseLLMException
return BaseLLMException(
message=error_message, status_code=status_code, headers=headers
)

View file

@ -1077,8 +1077,10 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
elif (
finish_reason and finish_reason in mapped_finish_reason.keys()
): # vertex ai
return mapped_finish_reason[finish_reason]
else:
return "stop"
@staticmethod
@ -1261,7 +1263,6 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
status_code=422,
headers=raw_response.headers,
)
return self._transform_google_generate_content_to_openai_model_response(
completion_response=completion_response,
@ -1270,7 +1271,6 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
logging_obj=logging_obj,
raw_response=raw_response,
)
def _transform_google_generate_content_to_openai_model_response(
self,

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
!function(){"use strict";var e,t,n,r,o,u,i,c,f,a={},l={};function d(e){var t=l[e];if(void 0!==t)return t.exports;var n=l[e]={id:e,loaded:!1,exports:{}},r=!0;try{a[e].call(n.exports,n,n.exports,d),r=!1}finally{r&&delete l[e]}return n.loaded=!0,n.exports}d.m=a,e=[],d.O=function(t,n,r,o){if(n){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,r,o];return}for(var i=1/0,u=0;u<e.length;u++){for(var n=e[u][0],r=e[u][1],o=e[u][2],c=!0,f=0;f<n.length;f++)i>=o&&Object.keys(d.O).every(function(e){return d.O[e](n[f])})?n.splice(f--,1):(c=!1,o<i&&(i=o));if(c){e.splice(u--,1);var a=r();void 0!==a&&(t=a)}}return t},d.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return d.d(t,{a:t}),t},n=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},d.t=function(e,r){if(1&r&&(e=this(e)),8&r||"object"==typeof e&&e&&(4&r&&e.__esModule||16&r&&"function"==typeof e.then))return e;var o=Object.create(null);d.r(o);var u={};t=t||[null,n({}),n([]),n(n)];for(var i=2&r&&e;"object"==typeof i&&!~t.indexOf(i);i=n(i))Object.getOwnPropertyNames(i).forEach(function(t){u[t]=function(){return e[t]}});return u.default=function(){return e},d.d(o,u),o},d.d=function(e,t){for(var n in t)d.o(t,n)&&!d.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},d.f={},d.e=function(e){return Promise.all(Object.keys(d.f).reduce(function(t,n){return d.f[n](e,t),t},[]))},d.u=function(e){},d.miniCssF=function(e){},d.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),d.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r={},o="_N_E:",d.l=function(e,t,n,u){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var i,c,f=document.getElementsByTagName("script"),a=0;a<f.length;a++){var l=f[a];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==o+n){i=l;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,d.nc&&i.setAttribute("nonce",d.nc),i.setAttribute("data-webpack",o+n),i.src=d.tu(e)),r[e]=[t];var s=function(t,n){i.onerror=i.onload=null,clearTimeout(p);var o=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach(function(e){return e(n)}),t)return t(n)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=s.bind(null,i.onerror),i.onload=s.bind(null,i.onload),c&&document.head.appendChild(i)},d.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},d.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},d.tt=function(){return void 0===u&&(u={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(u=trustedTypes.createPolicy("nextjs#bundler",u))),u},d.tu=function(e){return d.tt().createScriptURL(e)},d.p="/litellm-asset-prefix/_next/",i={272:0,919:0,986:0},d.f.j=function(e,t){var n=d.o(i,e)?i[e]:void 0;if(0!==n){if(n)t.push(n[2]);else if(/^(272|919|986)$/.test(e))i[e]=0;else{var r=new Promise(function(t,r){n=i[e]=[t,r]});t.push(n[2]=r);var o=d.p+d.u(e),u=Error();d.l(o,function(t){if(d.o(i,e)&&(0!==(n=i[e])&&(i[e]=void 0),n)){var r=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;u.message="Loading chunk "+e+" failed.\n("+r+": "+o+")",u.name="ChunkLoadError",u.type=r,u.request=o,n[1](u)}},"chunk-"+e,e)}}},d.O.j=function(e){return 0===i[e]},c=function(e,t){var n,r,o=t[0],u=t[1],c=t[2],f=0;if(o.some(function(e){return 0!==i[e]})){for(n in u)d.o(u,n)&&(d.m[n]=u[n]);if(c)var a=c(d)}for(e&&e(t);f<o.length;f++)r=o[f],d.o(i,r)&&i[r]&&i[r][0](),i[r]=0;return d.O(a)},(f=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(c.bind(null,0)),f.push=c.bind(null,f.push.bind(f))}();
!function(){"use strict";var e,t,n,r,o,u,i,c,f,a={},l={};function d(e){var t=l[e];if(void 0!==t)return t.exports;var n=l[e]={id:e,loaded:!1,exports:{}},r=!0;try{a[e].call(n.exports,n,n.exports,d),r=!1}finally{r&&delete l[e]}return n.loaded=!0,n.exports}d.m=a,e=[],d.O=function(t,n,r,o){if(n){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,r,o];return}for(var i=1/0,u=0;u<e.length;u++){for(var n=e[u][0],r=e[u][1],o=e[u][2],c=!0,f=0;f<n.length;f++)i>=o&&Object.keys(d.O).every(function(e){return d.O[e](n[f])})?n.splice(f--,1):(c=!1,o<i&&(i=o));if(c){e.splice(u--,1);var a=r();void 0!==a&&(t=a)}}return t},d.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return d.d(t,{a:t}),t},n=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},d.t=function(e,r){if(1&r&&(e=this(e)),8&r||"object"==typeof e&&e&&(4&r&&e.__esModule||16&r&&"function"==typeof e.then))return e;var o=Object.create(null);d.r(o);var u={};t=t||[null,n({}),n([]),n(n)];for(var i=2&r&&e;"object"==typeof i&&!~t.indexOf(i);i=n(i))Object.getOwnPropertyNames(i).forEach(function(t){u[t]=function(){return e[t]}});return u.default=function(){return e},d.d(o,u),o},d.d=function(e,t){for(var n in t)d.o(t,n)&&!d.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},d.f={},d.e=function(e){return Promise.all(Object.keys(d.f).reduce(function(t,n){return d.f[n](e,t),t},[]))},d.u=function(e){},d.miniCssF=function(e){},d.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),d.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r={},o="_N_E:",d.l=function(e,t,n,u){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var i,c,f=document.getElementsByTagName("script"),a=0;a<f.length;a++){var l=f[a];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==o+n){i=l;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,d.nc&&i.setAttribute("nonce",d.nc),i.setAttribute("data-webpack",o+n),i.src=d.tu(e)),r[e]=[t];var s=function(t,n){i.onerror=i.onload=null,clearTimeout(p);var o=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach(function(e){return e(n)}),t)return t(n)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=s.bind(null,i.onerror),i.onload=s.bind(null,i.onload),c&&document.head.appendChild(i)},d.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},d.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},d.tt=function(){return void 0===u&&(u={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(u=trustedTypes.createPolicy("nextjs#bundler",u))),u},d.tu=function(e){return d.tt().createScriptURL(e)},d.p="/_next/",i={272:0,919:0,986:0},d.f.j=function(e,t){var n=d.o(i,e)?i[e]:void 0;if(0!==n){if(n)t.push(n[2]);else if(/^(272|919|986)$/.test(e))i[e]=0;else{var r=new Promise(function(t,r){n=i[e]=[t,r]});t.push(n[2]=r);var o=d.p+d.u(e),u=Error();d.l(o,function(t){if(d.o(i,e)&&(0!==(n=i[e])&&(i[e]=void 0),n)){var r=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;u.message="Loading chunk "+e+" failed.\n("+r+": "+o+")",u.name="ChunkLoadError",u.type=r,u.request=o,n[1](u)}},"chunk-"+e,e)}}},d.O.j=function(e){return 0===i[e]},c=function(e,t){var n,r,o=t[0],u=t[1],c=t[2],f=0;if(o.some(function(e){return 0!==i[e]})){for(n in u)d.o(u,n)&&(d.m[n]=u[n]);if(c)var a=c(d)}for(e&&e(t);f<o.length;f++)r=o[f],d.o(i,r)&&i[r]&&i[r][0](),i[r]=0;return d.O(a)},(f=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(c.bind(null,0)),f.push=c.bind(null,f.push.bind(f))}();

View file

@ -1 +1 @@
@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/litellm-asset-prefix/_next/static/media/55c55f0601d81cf3-s.woff2) format("woff2");unicode-range:u+0460-052f,u+1c80-1c8a,u+20b4,u+2de0-2dff,u+a640-a69f,u+fe2e-fe2f}@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/litellm-asset-prefix/_next/static/media/26a46d62cd723877-s.woff2) format("woff2");unicode-range:u+0301,u+0400-045f,u+0490-0491,u+04b0-04b1,u+2116}@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/litellm-asset-prefix/_next/static/media/97e0cb1ae144a2a9-s.woff2) format("woff2");unicode-range:u+1f??}@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/litellm-asset-prefix/_next/static/media/581909926a08bbc8-s.woff2) format("woff2");unicode-range:u+0370-0377,u+037a-037f,u+0384-038a,u+038c,u+038e-03a1,u+03a3-03ff}@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/litellm-asset-prefix/_next/static/media/df0a9ae256c0569c-s.woff2) format("woff2");unicode-range:u+0102-0103,u+0110-0111,u+0128-0129,u+0168-0169,u+01a0-01a1,u+01af-01b0,u+0300-0301,u+0303-0304,u+0308-0309,u+0323,u+0329,u+1ea0-1ef9,u+20ab}@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/litellm-asset-prefix/_next/static/media/8e9860b6e62d6359-s.woff2) format("woff2");unicode-range:u+0100-02ba,u+02bd-02c5,u+02c7-02cc,u+02ce-02d7,u+02dd-02ff,u+0304,u+0308,u+0329,u+1d00-1dbf,u+1e00-1e9f,u+1ef2-1eff,u+2020,u+20a0-20ab,u+20ad-20c0,u+2113,u+2c60-2c7f,u+a720-a7ff}@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/litellm-asset-prefix/_next/static/media/e4af272ccee01ff0-s.p.woff2) format("woff2");unicode-range:u+00??,u+0131,u+0152-0153,u+02bb-02bc,u+02c6,u+02da,u+02dc,u+0304,u+0308,u+0329,u+2000-206f,u+20ac,u+2122,u+2191,u+2193,u+2212,u+2215,u+feff,u+fffd}@font-face{font-family:__Inter_Fallback_b0dd8a;src:local("Arial");ascent-override:90.49%;descent-override:22.56%;line-gap-override:0.00%;size-adjust:107.06%}.__className_b0dd8a{font-family:__Inter_b0dd8a,__Inter_Fallback_b0dd8a;font-style:normal}
@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/_next/static/media/55c55f0601d81cf3-s.woff2) format("woff2");unicode-range:u+0460-052f,u+1c80-1c8a,u+20b4,u+2de0-2dff,u+a640-a69f,u+fe2e-fe2f}@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/_next/static/media/26a46d62cd723877-s.woff2) format("woff2");unicode-range:u+0301,u+0400-045f,u+0490-0491,u+04b0-04b1,u+2116}@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/_next/static/media/97e0cb1ae144a2a9-s.woff2) format("woff2");unicode-range:u+1f??}@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/_next/static/media/581909926a08bbc8-s.woff2) format("woff2");unicode-range:u+0370-0377,u+037a-037f,u+0384-038a,u+038c,u+038e-03a1,u+03a3-03ff}@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/_next/static/media/df0a9ae256c0569c-s.woff2) format("woff2");unicode-range:u+0102-0103,u+0110-0111,u+0128-0129,u+0168-0169,u+01a0-01a1,u+01af-01b0,u+0300-0301,u+0303-0304,u+0308-0309,u+0323,u+0329,u+1ea0-1ef9,u+20ab}@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/_next/static/media/8e9860b6e62d6359-s.woff2) format("woff2");unicode-range:u+0100-02ba,u+02bd-02c5,u+02c7-02cc,u+02ce-02d7,u+02dd-02ff,u+0304,u+0308,u+0329,u+1d00-1dbf,u+1e00-1e9f,u+1ef2-1eff,u+2020,u+20a0-20ab,u+20ad-20c0,u+2113,u+2c60-2c7f,u+a720-a7ff}@font-face{font-family:__Inter_b0dd8a;font-style:normal;font-weight:100 900;font-display:swap;src:url(/_next/static/media/e4af272ccee01ff0-s.p.woff2) format("woff2");unicode-range:u+00??,u+0131,u+0152-0153,u+02bb-02bc,u+02c6,u+02da,u+02dc,u+0304,u+0308,u+0329,u+2000-206f,u+20ac,u+2122,u+2191,u+2193,u+2212,u+2215,u+feff,u+fffd}@font-face{font-family:__Inter_Fallback_b0dd8a;src:local("Arial");ascent-override:90.49%;descent-override:22.56%;line-gap-override:0.00%;size-adjust:107.06%}.__className_b0dd8a{font-family:__Inter_b0dd8a,__Inter_Fallback_b0dd8a;font-style:normal}

View file

@ -1,34 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.0" id="katman_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 600 450" style="enable-background:new 0 0 600 450;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#343B45;}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#F4981A;}
</style>
<g id="_x31__stroke">
<g id="Amazon_1_">
<rect x="161.2" y="86.5" class="st0" width="277.8" height="277.8"/>
<g id="Amazon">
<path class="st1" d="M315,163.7c-8,0.6-17.2,1.2-26.4,2.4c-14.1,1.9-28.2,4.3-39.8,9.8c-22.7,9.2-38,28.8-38,57.6
c0,36.2,23.3,54.6,52.7,54.6c9.8,0,17.8-1.2,25.1-3.1c11.7-3.7,21.5-10.4,33.1-22.7c6.7,9.2,8.6,13.5,20.2,23.3
c3.1,1.2,6.1,1.2,8.6-0.6c7.4-6.1,20.3-17.2,27-23.3c3.1-2.5,2.5-6.1,0.6-9.2c-6.7-8.6-13.5-16-13.5-32.5V165
c0-23.3,1.9-44.8-15.3-60.7c-14.1-12.9-36.2-17.8-53.4-17.8h-7.4c-31.2,1.8-64.3,15.3-71.7,54c-1.2,4.9,2.5,6.8,4.9,7.4l34.3,4.3
c3.7-0.6,5.5-3.7,6.1-6.7c3.1-13.5,14.1-20.2,26.3-21.5h2.5c7.4,0,15.3,3.1,19.6,9.2c4.9,7.4,4.3,17.2,4.3,25.8L315,163.7
L315,163.7z M308.2,236.7c-4.3,8.6-11.7,14.1-19.6,16c-1.2,0-3.1,0.6-4.9,0.6c-13.5,0-21.4-10.4-21.4-25.8
c0-19.6,11.6-28.8,26.3-33.1c8-1.8,17.2-2.5,26.4-2.5v7.4C315,213.4,315.6,224.4,308.2,236.7z"/>
<path class="st2" d="M398.8,311.4c-1.4,0-2.8,0.3-4.1,0.9c-1.5,0.6-3,1.3-4.4,1.9l-2.1,0.9l-2.7,1.1v0
c-29.8,12.1-61.1,19.2-90.1,19.8c-1.1,0-2.1,0-3.2,0c-45.6,0-82.8-21.1-120.3-42c-1.3-0.7-2.7-1-4-1c-1.7,0-3.4,0.6-4.7,1.8
c-1.3,1.2-2,2.9-2,4.7c0,2.3,1.2,4.4,2.9,5.7c35.2,30.6,73.8,59,125.7,59c1,0,2,0,3.1,0c33-0.7,70.3-11.9,99.3-30.1l0.2-0.1
c3.8-2.3,7.6-4.9,11.2-7.7c2.2-1.6,3.8-4.2,3.8-6.9C407.2,314.6,403.2,311.4,398.8,311.4z M439,294.5L439,294.5
c-0.1-2.9-0.7-5.1-1.9-6.9l-0.1-0.2l-0.1-0.2c-1.2-1.3-2.4-1.8-3.7-2.4c-3.8-1.5-9.3-2.3-16-2.3c-4.8,0-10.1,0.5-15.4,1.6l0-0.4
l-5.3,1.8l-0.1,0l-3,1v0.1c-3.5,1.5-6.8,3.3-9.8,5.5c-1.9,1.4-3.4,3.2-3.5,6.1c0,1.5,0.7,3.3,2,4.3c1.3,1,2.8,1.4,4.1,1.4
c0.3,0,0.6,0,0.9-0.1l0.3,0l0.2,0c2.6-0.6,6.4-0.9,10.9-1.6c3.8-0.4,7.9-0.7,11.4-0.7c2.5,0,4.7,0.2,6.3,0.5
c0.8,0.2,1.3,0.4,1.6,0.5c0.1,0,0.2,0.1,0.2,0.1c0.1,0.2,0.2,0.8,0.1,1.5c0,2.9-1.2,8.4-2.9,13.7c-1.7,5.3-3.7,10.7-5,14.2
c-0.3,0.8-0.5,1.7-0.5,2.7c0,1.4,0.6,3.2,1.8,4.3c1.2,1.1,2.8,1.6,4.1,1.6h0.1c2,0,3.6-0.8,5.1-1.9
c13.6-12.2,18.3-31.7,18.5-42.6L439,294.5z"/>
</g>
</g>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.0" id="katman_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 600 450" style="enable-background:new 0 0 600 450;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#343B45;}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#F4981A;}
</style>
<g id="_x31__stroke">
<g id="Amazon_1_">
<rect x="161.2" y="86.5" class="st0" width="277.8" height="277.8"/>
<g id="Amazon">
<path class="st1" d="M315,163.7c-8,0.6-17.2,1.2-26.4,2.4c-14.1,1.9-28.2,4.3-39.8,9.8c-22.7,9.2-38,28.8-38,57.6
c0,36.2,23.3,54.6,52.7,54.6c9.8,0,17.8-1.2,25.1-3.1c11.7-3.7,21.5-10.4,33.1-22.7c6.7,9.2,8.6,13.5,20.2,23.3
c3.1,1.2,6.1,1.2,8.6-0.6c7.4-6.1,20.3-17.2,27-23.3c3.1-2.5,2.5-6.1,0.6-9.2c-6.7-8.6-13.5-16-13.5-32.5V165
c0-23.3,1.9-44.8-15.3-60.7c-14.1-12.9-36.2-17.8-53.4-17.8h-7.4c-31.2,1.8-64.3,15.3-71.7,54c-1.2,4.9,2.5,6.8,4.9,7.4l34.3,4.3
c3.7-0.6,5.5-3.7,6.1-6.7c3.1-13.5,14.1-20.2,26.3-21.5h2.5c7.4,0,15.3,3.1,19.6,9.2c4.9,7.4,4.3,17.2,4.3,25.8L315,163.7
L315,163.7z M308.2,236.7c-4.3,8.6-11.7,14.1-19.6,16c-1.2,0-3.1,0.6-4.9,0.6c-13.5,0-21.4-10.4-21.4-25.8
c0-19.6,11.6-28.8,26.3-33.1c8-1.8,17.2-2.5,26.4-2.5v7.4C315,213.4,315.6,224.4,308.2,236.7z"/>
<path class="st2" d="M398.8,311.4c-1.4,0-2.8,0.3-4.1,0.9c-1.5,0.6-3,1.3-4.4,1.9l-2.1,0.9l-2.7,1.1v0
c-29.8,12.1-61.1,19.2-90.1,19.8c-1.1,0-2.1,0-3.2,0c-45.6,0-82.8-21.1-120.3-42c-1.3-0.7-2.7-1-4-1c-1.7,0-3.4,0.6-4.7,1.8
c-1.3,1.2-2,2.9-2,4.7c0,2.3,1.2,4.4,2.9,5.7c35.2,30.6,73.8,59,125.7,59c1,0,2,0,3.1,0c33-0.7,70.3-11.9,99.3-30.1l0.2-0.1
c3.8-2.3,7.6-4.9,11.2-7.7c2.2-1.6,3.8-4.2,3.8-6.9C407.2,314.6,403.2,311.4,398.8,311.4z M439,294.5L439,294.5
c-0.1-2.9-0.7-5.1-1.9-6.9l-0.1-0.2l-0.1-0.2c-1.2-1.3-2.4-1.8-3.7-2.4c-3.8-1.5-9.3-2.3-16-2.3c-4.8,0-10.1,0.5-15.4,1.6l0-0.4
l-5.3,1.8l-0.1,0l-3,1v0.1c-3.5,1.5-6.8,3.3-9.8,5.5c-1.9,1.4-3.4,3.2-3.5,6.1c0,1.5,0.7,3.3,2,4.3c1.3,1,2.8,1.4,4.1,1.4
c0.3,0,0.6,0,0.9-0.1l0.3,0l0.2,0c2.6-0.6,6.4-0.9,10.9-1.6c3.8-0.4,7.9-0.7,11.4-0.7c2.5,0,4.7,0.2,6.3,0.5
c0.8,0.2,1.3,0.4,1.6,0.5c0.1,0,0.2,0.1,0.2,0.1c0.1,0.2,0.2,0.8,0.1,1.5c0,2.9-1.2,8.4-2.9,13.7c-1.7,5.3-3.7,10.7-5,14.2
c-0.3,0.8-0.5,1.7-0.5,2.7c0,1.4,0.6,3.2,1.8,4.3c1.2,1.1,2.8,1.6,4.1,1.6h0.1c2,0,3.6-0.8,5.1-1.9
c13.6-12.2,18.3-31.7,18.5-42.6L439,294.5z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1,89 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.0" id="katman_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 800 600" style="enable-background:new 0 0 800 600;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#F05A28;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#231F20;}
</style>
<g id="Contact">
<g id="Contact-us" transform="translate(-234.000000, -1114.000000)">
<g id="map" transform="translate(-6.000000, 1027.000000)">
<g id="Contact-box" transform="translate(190.000000, 36.000000)">
<g id="Group-26" transform="translate(50.000000, 51.000000)">
<g id="Group-3">
<path id="Fill-1" class="st0" d="M220.9,421c-17,0-33.1-3.4-47.8-9.5c-22-9.2-40.8-24.6-54.1-44c-13.3-19.4-21-42.7-21-67.9
c0-16.8,3.4-32.7,9.7-47.3c9.3-21.8,24.9-40.3,44.5-53.4c19.6-13.1,43.2-20.7,68.7-20.7v-18.3c-19.5,0-38.1,3.9-55.1,11
c-25.4,10.6-47,28.3-62.2,50.6c-15.3,22.3-24.2,49.2-24.2,78.1c0,19.3,4,37.7,11.1,54.4c10.7,25.1,28.7,46.4,51.2,61.5
c22.6,15.1,49.8,23.9,79.1,23.9V421z"/>
<path id="Fill-4" class="st0" d="M157.9,374.1c-11.5-9.6-20.1-21.2-25.9-33.9c-5.8-12.7-8.8-26.4-8.8-40.2
c0-11,1.9-22,5.6-32.5c3.8-10.5,9.4-20.5,17.1-29.6c9.6-11.4,21.3-20,34-25.8c12.7-5.8,26.6-8.7,40.4-8.7
c11,0,22.1,1.9,32.6,5.6c10.6,3.8,20.6,9.4,29.7,17l11.9-14.1c-10.8-9-22.8-15.8-35.4-20.2c-12.6-4.5-25.7-6.7-38.8-6.7
c-16.5,0-32.9,3.5-48.1,10.4c-15.2,6.9-29.1,17.2-40.5,30.7c-9.1,10.8-15.8,22.7-20.3,35.2c-4.5,12.5-6.7,25.6-6.7,38.7
c0,16.4,3.5,32.8,10.4,47.9c6.9,15.1,17.3,29,30.9,40.3L157.9,374.1z"/>
<path id="Fill-6" class="st0" d="M186.4,362.2c-12.1-6.4-21.6-15.7-28.1-26.6c-6.5-10.9-9.9-23.5-9.9-36.2
c0-11.2,2.6-22.5,8.3-33c6.4-12.1,15.8-21.5,26.8-27.9c11-6.5,23.6-9.9,36.4-9.9c11.2,0,22.6,2.6,33.2,8.2l8.6-16.3
c-13.3-7-27.7-10.4-41.9-10.3c-16.1,0-32,4.3-45.8,12.4c-13.8,8.1-25.7,20.1-33.7,35.2c-7,13.3-10.4,27.6-10.4,41.6
c0,16,4.3,31.8,12.5,45.5c8.2,13.8,20.2,25.5,35.4,33.5L186.4,362.2z"/>
<path id="Fill-8" class="st0" d="M221,344.6c-6.3,0-12.3-1.3-17.7-3.6c-8.2-3.4-15.1-9.2-20-16.5c-4.9-7.3-7.8-16-7.8-25.4
c0-6.3,1.3-12.3,3.6-17.7c3.4-8.1,9.2-15.1,16.5-20c7.3-4.9,16-7.8,25.4-7.8v-18.4c-8.8,0-17.2,1.8-24.9,5
c-11.5,4.9-21.2,12.9-28.1,23.1C161,273.6,157,286,157,299.2c0,8.8,1.8,17.2,5,24.9c4.9,11.5,13,21.2,23.2,28.1
C195.4,359,207.7,363,221,363V344.6z"/>
</g>
<g id="Group" transform="translate(22.000000, 13.000000)">
<path id="Fill-10" class="st1" d="M214,271.6c-2.1-2.2-4.4-4-6.7-5.3c-2.3-1.3-4.7-2-7.2-2c-3.4,0-6.3,0.6-9,1.8
c-2.6,1.2-4.9,2.8-6.8,4.9c-1.9,2-3.3,4.4-4.3,7c-1,2.6-1.4,5.4-1.4,8.2c0,2.8,0.5,5.6,1.4,8.2c1,2.6,2.4,5,4.3,7
c1.9,2,4.1,3.7,6.8,4.9c2.6,1.2,5.6,1.8,9,1.8c2.8,0,5.5-0.6,7.9-1.7c2.4-1.2,4.5-2.9,6.2-5.1l12.2,13.1
c-1.8,1.8-3.9,3.4-6.3,4.7c-2.4,1.3-4.8,2.4-7.2,3.2s-4.8,1.4-7,1.7c-2.2,0.4-4.2,0.5-5.8,0.5c-5.5,0-10.7-0.9-15.5-2.7
c-4.9-1.8-9.1-4.4-12.6-7.8c-3.6-3.3-6.4-7.4-8.5-12.1c-2.1-4.7-3.1-10-3.1-15.7c0-5.8,1-11,3.1-15.7
c2.1-4.7,4.9-8.7,8.5-12.1c3.6-3.3,7.8-5.9,12.6-7.8c4.9-1.8,10.1-2.7,15.5-2.7c4.7,0,9.4,0.9,14.1,2.7
c4.7,1.8,8.9,4.6,12.4,8.4L214,271.6z"/>
<path id="Fill-12" class="st1" d="M280.4,278.9c-0.1-5.4-1.8-9.6-5-12.7c-3.3-3.1-7.8-4.6-13.6-4.6c-5.5,0-9.8,1.6-13,4.7
c-3.2,3.1-5.2,7.4-5.9,12.6H280.4z M243,292.6c0.6,5.5,2.7,9.7,6.4,12.8c3.7,3,8.1,4.6,13.3,4.6c4.6,0,8.4-0.9,11.5-2.8
c3.1-1.9,5.8-4.2,8.2-7.1l13.1,9.9c-4.3,5.3-9,9-14.3,11.3c-5.3,2.2-10.8,3.3-16.6,3.3c-5.5,0-10.7-0.9-15.5-2.7
c-4.9-1.8-9.1-4.4-12.6-7.8c-3.6-3.3-6.4-7.4-8.5-12.1c-2.1-4.7-3.1-10-3.1-15.7c0-5.8,1-11,3.1-15.7
c2.1-4.7,4.9-8.7,8.5-12.1c3.6-3.3,7.8-5.9,12.6-7.8c4.9-1.8,10.1-2.7,15.5-2.7c5.1,0,9.7,0.9,13.9,2.7
c4.2,1.8,7.8,4.3,10.8,7.7c3,3.3,5.3,7.5,7,12.4c1.7,4.9,2.5,10.6,2.5,17v5H243z"/>
<path id="Fill-14" class="st1" d="M306.5,249.7h18.3v11.5h0.3c2-4.3,4.9-7.5,8.7-9.9c3.8-2.3,8.1-3.5,12.9-3.5
c1.1,0,2.2,0.1,3.3,0.3c1.1,0.2,2.2,0.5,3.3,0.8v17.6c-1.5-0.4-3-0.7-4.5-1c-1.5-0.3-2.9-0.4-4.3-0.4c-4.3,0-7.7,0.8-10.3,2.4
c-2.6,1.6-4.6,3.4-5.9,5.4c-1.4,2-2.3,4.1-2.7,6.1c-0.5,2-0.7,3.5-0.7,4.6v39h-18.3V249.7z"/>
<path id="Fill-16" class="st1" d="M409,278.9c-0.1-5.4-1.8-9.6-5-12.7c-3.3-3.1-7.8-4.6-13.6-4.6c-5.5,0-9.8,1.6-13,4.7
c-3.2,3.1-5.2,7.4-5.9,12.6H409z M371.6,292.6c0.6,5.5,2.7,9.7,6.4,12.8c3.7,3,8.1,4.6,13.3,4.6c4.6,0,8.4-0.9,11.5-2.8
c3.1-1.9,5.8-4.2,8.2-7.1l13.1,9.9c-4.3,5.3-9,9-14.3,11.3c-5.3,2.2-10.8,3.3-16.6,3.3c-5.5,0-10.7-0.9-15.5-2.7
c-4.9-1.8-9.1-4.4-12.6-7.8c-3.6-3.3-6.4-7.4-8.5-12.1c-2.1-4.7-3.1-10-3.1-15.7c0-5.8,1-11,3.1-15.7
c2.1-4.7,4.9-8.7,8.5-12.1c3.6-3.3,7.8-5.9,12.6-7.8c4.9-1.8,10.1-2.7,15.5-2.7c5.1,0,9.7,0.9,13.9,2.7
c4.2,1.8,7.8,4.3,10.8,7.7c3,3.3,5.3,7.5,7,12.4c1.7,4.9,2.5,10.6,2.5,17v5H371.6z"/>
<path id="Fill-18" class="st1" d="M494.6,286.2c0-2.8-0.5-5.6-1.5-8.2c-1-2.6-2.4-5-4.3-7c-1.9-2-4.2-3.7-6.9-4.9
c-2.7-1.2-5.7-1.8-9.1-1.8c-3.4,0-6.4,0.6-9.1,1.8c-2.7,1.2-5,2.8-6.9,4.9c-1.9,2-3.3,4.4-4.3,7c-1,2.6-1.5,5.4-1.5,8.2
c0,2.8,0.5,5.6,1.5,8.2c1,2.6,2.4,5,4.3,7c1.9,2,4.2,3.7,6.9,4.9c2.7,1.2,5.7,1.8,9.1,1.8c3.4,0,6.4-0.6,9.1-1.8
c2.7-1.2,5-2.8,6.9-4.9c1.9-2,3.3-4.4,4.3-7C494.1,291.8,494.6,289,494.6,286.2L494.6,286.2z M433.2,207.6h18.5v51.3h0.5
c0.9-1.2,2.1-2.5,3.5-3.7c1.4-1.3,3.2-2.5,5.2-3.6c2.1-1.1,4.4-2,7.1-2.7c2.7-0.7,5.8-1.1,9.3-1.1c5.2,0,10.1,1,14.5,3
c4.4,2,8.2,4.7,11.3,8.1c3.1,3.5,5.6,7.5,7.3,12.2c1.7,4.7,2.6,9.7,2.6,15.1c0,5.4-0.8,10.4-2.5,15.1
c-1.6,4.7-4.1,8.7-7.2,12.2c-3.2,3.5-7,6.2-11.6,8.1c-4.5,2-9.6,3-15.3,3c-5.2,0-10.1-1-14.7-3c-4.5-2-8.1-5.3-10.8-9.7h-0.3
v11h-17.6V207.6z"/>
<path id="Fill-20" class="st1" d="M520.9,249.7h18.3v11.5h0.3c2-4.3,4.9-7.5,8.7-9.9c3.8-2.3,8.1-3.5,12.9-3.5
c1.1,0,2.2,0.1,3.3,0.3c1.1,0.2,2.2,0.5,3.3,0.8v17.6c-1.5-0.4-3-0.7-4.5-1c-1.5-0.3-2.9-0.4-4.3-0.4c-4.3,0-7.7,0.8-10.3,2.4
c-2.6,1.6-4.6,3.4-5.9,5.4c-1.4,2-2.3,4.1-2.7,6.1c-0.5,2-0.7,3.5-0.7,4.6v39h-18.3V249.7z"/>
<path id="Fill-22" class="st1" d="M616,290h-3.9c-2.6,0-5.5,0.1-8.7,0.3c-3.2,0.2-6.2,0.7-9.1,1.4c-2.8,0.8-5.2,1.9-7.2,3.3
c-2,1.5-2.9,3.5-2.9,6.2c0,1.7,0.4,3.2,1.2,4.3c0.8,1.2,1.8,2.2,3,3c1.2,0.8,2.6,1.4,4.2,1.8c1.5,0.4,3.1,0.5,4.6,0.5
c6.4,0,11.1-1.5,14.2-4.5c3-3,4.6-7.1,4.6-12.2V290z M617.1,312.7h-0.5c-2.7,4.2-6.1,7.2-10.2,9.1c-4.1,1.9-8.7,2.8-13.6,2.8
c-3.4,0-6.7-0.5-10-1.4s-6.1-2.3-8.7-4.1c-2.5-1.8-4.6-4.1-6.1-6.8s-2.3-5.9-2.3-9.6c0-4,0.7-7.3,2.2-10.1
c1.4-2.8,3.4-5.1,5.8-7c2.4-1.9,5.2-3.4,8.4-4.5c3.2-1.1,6.5-2,10-2.5c3.5-0.6,6.9-0.9,10.5-1.1c3.5-0.2,6.8-0.2,9.9-0.2h4.6
v-2c0-4.6-1.6-8-4.8-10.3c-3.2-2.3-7.3-3.4-12.2-3.4c-3.9,0-7.6,0.7-11,2.1c-3.4,1.4-6.4,3.2-8.8,5.6l-9.8-9.6
c4.1-4.2,9-7.1,14.5-9c5.5-1.8,11.2-2.7,17.1-2.7c5.3,0,9.7,0.6,13.3,1.7c3.6,1.2,6.6,2.7,9,4.5c2.4,1.8,4.2,3.9,5.5,6.3
c1.3,2.4,2.2,4.8,2.8,7.2c0.6,2.4,0.9,4.8,1,7.1c0.1,2.3,0.2,4.3,0.2,6v42h-16.7V312.7z"/>
<path id="Fill-24" class="st1" d="M683.6,269.9c-3.6-5-8.4-7.5-14.4-7.5c-2.5,0-4.9,0.6-7.2,1.8c-2.4,1.2-3.5,3.2-3.5,5.9
c0,2.2,1,3.9,2.9,4.9c1.9,1,4.4,1.9,7.4,2.6c3,0.7,6.2,1.4,9.6,2.2c3.4,0.8,6.6,1.9,9.6,3.5c3,1.6,5.4,3.7,7.4,6.5
c1.9,2.7,2.9,6.5,2.9,11.3c0,4.4-0.9,8-2.8,11c-1.9,3-4.3,5.4-7.4,7.2c-3,1.8-6.4,3.1-10.2,4c-3.8,0.8-7.6,1.2-11.3,1.2
c-5.7,0-11-0.8-15.8-2.4c-4.8-1.6-9.1-4.6-12.9-8.8l12.3-11.4c2.4,2.6,4.9,4.8,7.6,6.5c2.7,1.7,6,2.5,9.9,2.5
c1.3,0,2.7-0.2,4.1-0.5c1.4-0.3,2.8-0.8,4-1.5c1.2-0.7,2.2-1.6,3-2.7c0.8-1.1,1.1-2.3,1.1-3.7c0-2.5-1-4.4-2.9-5.6
c-1.9-1.2-4.4-2.2-7.4-3c-3-0.8-6.2-1.5-9.6-2.1c-3.4-0.7-6.6-1.7-9.6-3.2c-3-1.5-5.4-3.5-7.4-6.2c-1.9-2.6-2.9-6.3-2.9-11
c0-4.1,0.8-7.6,2.5-10.6c1.7-3,3.9-5.4,6.7-7.4c2.8-1.9,5.9-3.3,9.5-4.3c3.6-0.9,7.2-1.4,10.9-1.4c4.9,0,9.8,0.8,14.6,2.5
c4.8,1.7,8.7,4.5,11.7,8.6L683.6,269.9z"/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.0" id="katman_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 800 600" style="enable-background:new 0 0 800 600;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#F05A28;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#231F20;}
</style>
<g id="Contact">
<g id="Contact-us" transform="translate(-234.000000, -1114.000000)">
<g id="map" transform="translate(-6.000000, 1027.000000)">
<g id="Contact-box" transform="translate(190.000000, 36.000000)">
<g id="Group-26" transform="translate(50.000000, 51.000000)">
<g id="Group-3">
<path id="Fill-1" class="st0" d="M220.9,421c-17,0-33.1-3.4-47.8-9.5c-22-9.2-40.8-24.6-54.1-44c-13.3-19.4-21-42.7-21-67.9
c0-16.8,3.4-32.7,9.7-47.3c9.3-21.8,24.9-40.3,44.5-53.4c19.6-13.1,43.2-20.7,68.7-20.7v-18.3c-19.5,0-38.1,3.9-55.1,11
c-25.4,10.6-47,28.3-62.2,50.6c-15.3,22.3-24.2,49.2-24.2,78.1c0,19.3,4,37.7,11.1,54.4c10.7,25.1,28.7,46.4,51.2,61.5
c22.6,15.1,49.8,23.9,79.1,23.9V421z"/>
<path id="Fill-4" class="st0" d="M157.9,374.1c-11.5-9.6-20.1-21.2-25.9-33.9c-5.8-12.7-8.8-26.4-8.8-40.2
c0-11,1.9-22,5.6-32.5c3.8-10.5,9.4-20.5,17.1-29.6c9.6-11.4,21.3-20,34-25.8c12.7-5.8,26.6-8.7,40.4-8.7
c11,0,22.1,1.9,32.6,5.6c10.6,3.8,20.6,9.4,29.7,17l11.9-14.1c-10.8-9-22.8-15.8-35.4-20.2c-12.6-4.5-25.7-6.7-38.8-6.7
c-16.5,0-32.9,3.5-48.1,10.4c-15.2,6.9-29.1,17.2-40.5,30.7c-9.1,10.8-15.8,22.7-20.3,35.2c-4.5,12.5-6.7,25.6-6.7,38.7
c0,16.4,3.5,32.8,10.4,47.9c6.9,15.1,17.3,29,30.9,40.3L157.9,374.1z"/>
<path id="Fill-6" class="st0" d="M186.4,362.2c-12.1-6.4-21.6-15.7-28.1-26.6c-6.5-10.9-9.9-23.5-9.9-36.2
c0-11.2,2.6-22.5,8.3-33c6.4-12.1,15.8-21.5,26.8-27.9c11-6.5,23.6-9.9,36.4-9.9c11.2,0,22.6,2.6,33.2,8.2l8.6-16.3
c-13.3-7-27.7-10.4-41.9-10.3c-16.1,0-32,4.3-45.8,12.4c-13.8,8.1-25.7,20.1-33.7,35.2c-7,13.3-10.4,27.6-10.4,41.6
c0,16,4.3,31.8,12.5,45.5c8.2,13.8,20.2,25.5,35.4,33.5L186.4,362.2z"/>
<path id="Fill-8" class="st0" d="M221,344.6c-6.3,0-12.3-1.3-17.7-3.6c-8.2-3.4-15.1-9.2-20-16.5c-4.9-7.3-7.8-16-7.8-25.4
c0-6.3,1.3-12.3,3.6-17.7c3.4-8.1,9.2-15.1,16.5-20c7.3-4.9,16-7.8,25.4-7.8v-18.4c-8.8,0-17.2,1.8-24.9,5
c-11.5,4.9-21.2,12.9-28.1,23.1C161,273.6,157,286,157,299.2c0,8.8,1.8,17.2,5,24.9c4.9,11.5,13,21.2,23.2,28.1
C195.4,359,207.7,363,221,363V344.6z"/>
</g>
<g id="Group" transform="translate(22.000000, 13.000000)">
<path id="Fill-10" class="st1" d="M214,271.6c-2.1-2.2-4.4-4-6.7-5.3c-2.3-1.3-4.7-2-7.2-2c-3.4,0-6.3,0.6-9,1.8
c-2.6,1.2-4.9,2.8-6.8,4.9c-1.9,2-3.3,4.4-4.3,7c-1,2.6-1.4,5.4-1.4,8.2c0,2.8,0.5,5.6,1.4,8.2c1,2.6,2.4,5,4.3,7
c1.9,2,4.1,3.7,6.8,4.9c2.6,1.2,5.6,1.8,9,1.8c2.8,0,5.5-0.6,7.9-1.7c2.4-1.2,4.5-2.9,6.2-5.1l12.2,13.1
c-1.8,1.8-3.9,3.4-6.3,4.7c-2.4,1.3-4.8,2.4-7.2,3.2s-4.8,1.4-7,1.7c-2.2,0.4-4.2,0.5-5.8,0.5c-5.5,0-10.7-0.9-15.5-2.7
c-4.9-1.8-9.1-4.4-12.6-7.8c-3.6-3.3-6.4-7.4-8.5-12.1c-2.1-4.7-3.1-10-3.1-15.7c0-5.8,1-11,3.1-15.7
c2.1-4.7,4.9-8.7,8.5-12.1c3.6-3.3,7.8-5.9,12.6-7.8c4.9-1.8,10.1-2.7,15.5-2.7c4.7,0,9.4,0.9,14.1,2.7
c4.7,1.8,8.9,4.6,12.4,8.4L214,271.6z"/>
<path id="Fill-12" class="st1" d="M280.4,278.9c-0.1-5.4-1.8-9.6-5-12.7c-3.3-3.1-7.8-4.6-13.6-4.6c-5.5,0-9.8,1.6-13,4.7
c-3.2,3.1-5.2,7.4-5.9,12.6H280.4z M243,292.6c0.6,5.5,2.7,9.7,6.4,12.8c3.7,3,8.1,4.6,13.3,4.6c4.6,0,8.4-0.9,11.5-2.8
c3.1-1.9,5.8-4.2,8.2-7.1l13.1,9.9c-4.3,5.3-9,9-14.3,11.3c-5.3,2.2-10.8,3.3-16.6,3.3c-5.5,0-10.7-0.9-15.5-2.7
c-4.9-1.8-9.1-4.4-12.6-7.8c-3.6-3.3-6.4-7.4-8.5-12.1c-2.1-4.7-3.1-10-3.1-15.7c0-5.8,1-11,3.1-15.7
c2.1-4.7,4.9-8.7,8.5-12.1c3.6-3.3,7.8-5.9,12.6-7.8c4.9-1.8,10.1-2.7,15.5-2.7c5.1,0,9.7,0.9,13.9,2.7
c4.2,1.8,7.8,4.3,10.8,7.7c3,3.3,5.3,7.5,7,12.4c1.7,4.9,2.5,10.6,2.5,17v5H243z"/>
<path id="Fill-14" class="st1" d="M306.5,249.7h18.3v11.5h0.3c2-4.3,4.9-7.5,8.7-9.9c3.8-2.3,8.1-3.5,12.9-3.5
c1.1,0,2.2,0.1,3.3,0.3c1.1,0.2,2.2,0.5,3.3,0.8v17.6c-1.5-0.4-3-0.7-4.5-1c-1.5-0.3-2.9-0.4-4.3-0.4c-4.3,0-7.7,0.8-10.3,2.4
c-2.6,1.6-4.6,3.4-5.9,5.4c-1.4,2-2.3,4.1-2.7,6.1c-0.5,2-0.7,3.5-0.7,4.6v39h-18.3V249.7z"/>
<path id="Fill-16" class="st1" d="M409,278.9c-0.1-5.4-1.8-9.6-5-12.7c-3.3-3.1-7.8-4.6-13.6-4.6c-5.5,0-9.8,1.6-13,4.7
c-3.2,3.1-5.2,7.4-5.9,12.6H409z M371.6,292.6c0.6,5.5,2.7,9.7,6.4,12.8c3.7,3,8.1,4.6,13.3,4.6c4.6,0,8.4-0.9,11.5-2.8
c3.1-1.9,5.8-4.2,8.2-7.1l13.1,9.9c-4.3,5.3-9,9-14.3,11.3c-5.3,2.2-10.8,3.3-16.6,3.3c-5.5,0-10.7-0.9-15.5-2.7
c-4.9-1.8-9.1-4.4-12.6-7.8c-3.6-3.3-6.4-7.4-8.5-12.1c-2.1-4.7-3.1-10-3.1-15.7c0-5.8,1-11,3.1-15.7
c2.1-4.7,4.9-8.7,8.5-12.1c3.6-3.3,7.8-5.9,12.6-7.8c4.9-1.8,10.1-2.7,15.5-2.7c5.1,0,9.7,0.9,13.9,2.7
c4.2,1.8,7.8,4.3,10.8,7.7c3,3.3,5.3,7.5,7,12.4c1.7,4.9,2.5,10.6,2.5,17v5H371.6z"/>
<path id="Fill-18" class="st1" d="M494.6,286.2c0-2.8-0.5-5.6-1.5-8.2c-1-2.6-2.4-5-4.3-7c-1.9-2-4.2-3.7-6.9-4.9
c-2.7-1.2-5.7-1.8-9.1-1.8c-3.4,0-6.4,0.6-9.1,1.8c-2.7,1.2-5,2.8-6.9,4.9c-1.9,2-3.3,4.4-4.3,7c-1,2.6-1.5,5.4-1.5,8.2
c0,2.8,0.5,5.6,1.5,8.2c1,2.6,2.4,5,4.3,7c1.9,2,4.2,3.7,6.9,4.9c2.7,1.2,5.7,1.8,9.1,1.8c3.4,0,6.4-0.6,9.1-1.8
c2.7-1.2,5-2.8,6.9-4.9c1.9-2,3.3-4.4,4.3-7C494.1,291.8,494.6,289,494.6,286.2L494.6,286.2z M433.2,207.6h18.5v51.3h0.5
c0.9-1.2,2.1-2.5,3.5-3.7c1.4-1.3,3.2-2.5,5.2-3.6c2.1-1.1,4.4-2,7.1-2.7c2.7-0.7,5.8-1.1,9.3-1.1c5.2,0,10.1,1,14.5,3
c4.4,2,8.2,4.7,11.3,8.1c3.1,3.5,5.6,7.5,7.3,12.2c1.7,4.7,2.6,9.7,2.6,15.1c0,5.4-0.8,10.4-2.5,15.1
c-1.6,4.7-4.1,8.7-7.2,12.2c-3.2,3.5-7,6.2-11.6,8.1c-4.5,2-9.6,3-15.3,3c-5.2,0-10.1-1-14.7-3c-4.5-2-8.1-5.3-10.8-9.7h-0.3
v11h-17.6V207.6z"/>
<path id="Fill-20" class="st1" d="M520.9,249.7h18.3v11.5h0.3c2-4.3,4.9-7.5,8.7-9.9c3.8-2.3,8.1-3.5,12.9-3.5
c1.1,0,2.2,0.1,3.3,0.3c1.1,0.2,2.2,0.5,3.3,0.8v17.6c-1.5-0.4-3-0.7-4.5-1c-1.5-0.3-2.9-0.4-4.3-0.4c-4.3,0-7.7,0.8-10.3,2.4
c-2.6,1.6-4.6,3.4-5.9,5.4c-1.4,2-2.3,4.1-2.7,6.1c-0.5,2-0.7,3.5-0.7,4.6v39h-18.3V249.7z"/>
<path id="Fill-22" class="st1" d="M616,290h-3.9c-2.6,0-5.5,0.1-8.7,0.3c-3.2,0.2-6.2,0.7-9.1,1.4c-2.8,0.8-5.2,1.9-7.2,3.3
c-2,1.5-2.9,3.5-2.9,6.2c0,1.7,0.4,3.2,1.2,4.3c0.8,1.2,1.8,2.2,3,3c1.2,0.8,2.6,1.4,4.2,1.8c1.5,0.4,3.1,0.5,4.6,0.5
c6.4,0,11.1-1.5,14.2-4.5c3-3,4.6-7.1,4.6-12.2V290z M617.1,312.7h-0.5c-2.7,4.2-6.1,7.2-10.2,9.1c-4.1,1.9-8.7,2.8-13.6,2.8
c-3.4,0-6.7-0.5-10-1.4s-6.1-2.3-8.7-4.1c-2.5-1.8-4.6-4.1-6.1-6.8s-2.3-5.9-2.3-9.6c0-4,0.7-7.3,2.2-10.1
c1.4-2.8,3.4-5.1,5.8-7c2.4-1.9,5.2-3.4,8.4-4.5c3.2-1.1,6.5-2,10-2.5c3.5-0.6,6.9-0.9,10.5-1.1c3.5-0.2,6.8-0.2,9.9-0.2h4.6
v-2c0-4.6-1.6-8-4.8-10.3c-3.2-2.3-7.3-3.4-12.2-3.4c-3.9,0-7.6,0.7-11,2.1c-3.4,1.4-6.4,3.2-8.8,5.6l-9.8-9.6
c4.1-4.2,9-7.1,14.5-9c5.5-1.8,11.2-2.7,17.1-2.7c5.3,0,9.7,0.6,13.3,1.7c3.6,1.2,6.6,2.7,9,4.5c2.4,1.8,4.2,3.9,5.5,6.3
c1.3,2.4,2.2,4.8,2.8,7.2c0.6,2.4,0.9,4.8,1,7.1c0.1,2.3,0.2,4.3,0.2,6v42h-16.7V312.7z"/>
<path id="Fill-24" class="st1" d="M683.6,269.9c-3.6-5-8.4-7.5-14.4-7.5c-2.5,0-4.9,0.6-7.2,1.8c-2.4,1.2-3.5,3.2-3.5,5.9
c0,2.2,1,3.9,2.9,4.9c1.9,1,4.4,1.9,7.4,2.6c3,0.7,6.2,1.4,9.6,2.2c3.4,0.8,6.6,1.9,9.6,3.5c3,1.6,5.4,3.7,7.4,6.5
c1.9,2.7,2.9,6.5,2.9,11.3c0,4.4-0.9,8-2.8,11c-1.9,3-4.3,5.4-7.4,7.2c-3,1.8-6.4,3.1-10.2,4c-3.8,0.8-7.6,1.2-11.3,1.2
c-5.7,0-11-0.8-15.8-2.4c-4.8-1.6-9.1-4.6-12.9-8.8l12.3-11.4c2.4,2.6,4.9,4.8,7.6,6.5c2.7,1.7,6,2.5,9.9,2.5
c1.3,0,2.7-0.2,4.1-0.5c1.4-0.3,2.8-0.8,4-1.5c1.2-0.7,2.2-1.6,3-2.7c0.8-1.1,1.1-2.3,1.1-3.7c0-2.5-1-4.4-2.9-5.6
c-1.9-1.2-4.4-2.2-7.4-3c-3-0.8-6.2-1.5-9.6-2.1c-3.4-0.7-6.6-1.7-9.6-3.2c-3-1.5-5.4-3.5-7.4-6.2c-1.9-2.6-2.9-6.3-2.9-11
c0-4.1,0.8-7.6,2.5-10.6c1.7-3,3.9-5.4,6.7-7.4c2.8-1.9,5.9-3.3,9.5-4.3c3.6-0.9,7.2-1.4,10.9-1.4c4.9,0,9.8,0.8,14.6,2.5
c4.8,1.7,8.7,4.5,11.7,8.6L683.6,269.9z"/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -1,25 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 292.6 215.3" style="enable-background:new 0 0 292.6 215.3;" xml:space="preserve">
<style type="text/css">
.st0{fill:#566AB2;}
</style>
<path class="st0" d="M191.3,123.7c-2.4,1-4.9,1.8-7.2,1.9c-3.6,0.2-7.6-1.3-9.7-3.1c-3.3-2.8-5.7-4.4-6.7-9.2
c-0.4-2.1-0.2-5.3,0.2-7.2c0.9-4-0.1-6.5-2.9-8.9c-2.3-1.9-5.2-2.4-8.4-2.4s-2.3-0.5-3.1-1c-1.3-0.7-2.4-2.3-1.4-4.4
c0.3-0.7,2-2.3,2.3-2.5c4.3-2.5,9.4-1.7,14,0.2c4.3,1.7,7.5,5,12.2,9.5c4.8,5.5,5.6,7,8.4,11.1c2.1,3.2,4.1,6.6,5.4,10.4
C195.2,120.5,194.2,122.4,191.3,123.7L191.3,123.7z M153.4,104.3c0-2.1,1.7-3.7,3.8-3.7s0.9,0.1,1.3,0.2c0.5,0.2,1,0.5,1.4,0.9
c0.7,0.7,1.1,1.6,1.1,2.6c0,2.1-1.7,3.8-3.8,3.8s-3.7-1.7-3.7-3.8H153.4z M141.2,182.8c-25.5-20-37.8-26.6-42.9-26.3
c-4.8,0.3-3.9,5.7-2.8,9.3c1.1,3.5,2.5,5.9,4.5,9c1.4,2,2.3,5.1-1.4,7.3c-8.2,5.1-22.5-1.7-23.1-2c-16.6-9.8-30.5-22.7-40.2-40.3
c-9.5-17-14.9-35.2-15.8-54.6c-0.2-4.7,1.1-6.4,5.8-7.2c6.2-1.1,12.5-1.4,18.7-0.5c26,3.8,48.1,15.4,66.7,33.8
c10.6,10.5,18.6,23,26.8,35.2c8.8,13,18.2,25.4,30.2,35.5c4.3,3.6,7.6,6.3,10.9,8.2c-9.8,1.1-26.1,1.3-37.2-7.5L141.2,182.8z
M289.5,18c-3.1-1.5-4.4,1.4-6.3,2.8c-0.6,0.5-1.1,1.1-1.7,1.7c-4.5,4.8-9.8,8-16.8,7.6c-10.1-0.6-18.7,2.6-26.4,10.4
c-1.6-9.5-7-15.2-15.2-18.9c-4.3-1.9-8.6-3.8-11.6-7.9c-2.1-2.9-2.7-6.2-3.7-9.4c-0.7-2-1.3-3.9-3.6-4.3c-2.4-0.4-3.4,1.7-4.3,3.4
c-3.8,7-5.3,14.6-5.2,22.4c0.3,17.5,7.7,31.5,22.4,41.4c1.7,1.1,2.1,2.3,1.6,3.9c-1,3.4-2.2,6.7-3.3,10.1c-0.7,2.2-1.7,2.7-4,1.7
c-8.1-3.4-15-8.4-21.2-14.4c-10.4-10.1-19.9-21.2-31.6-30c-2.8-2.1-5.5-4-8.4-5.7c-12-11.7,1.6-21.3,4.7-22.4
c3.3-1.2,1.2-5.3-9.5-5.2c-10.6,0-20.3,3.6-32.8,8.4c-1.8,0.7-3.7,1.2-5.7,1.7c-11.3-2.1-22.9-2.6-35.1-1.2
c-23,2.5-41.4,13.4-54.8,32C1,68.3-2.8,93.6,1.9,120c4.9,27.8,19.1,50.9,41,68.9c22.6,18.7,48.7,27.8,78.5,26.1
c18.1-1,38.2-3.5,60.9-22.7c5.7,2.8,11.7,4,21.7,4.8c7.7,0.7,15.1-0.4,20.8-1.5c9-1.9,8.4-10.2,5.1-11.7
c-26.3-12.3-20.5-7.3-25.7-11.3c13.3-15.8,33.5-32.2,41.3-85.4c0.6-4.2,0.1-6.9,0-10.3c0-2.1,0.4-2.9,2.8-3.1
c6.6-0.8,13-2.6,18.8-5.8c17-9.3,23.9-24.6,25.5-42.9c0.2-2.8,0-5.7-3-7.2L289.5,18z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 292.6 215.3" style="enable-background:new 0 0 292.6 215.3;" xml:space="preserve">
<style type="text/css">
.st0{fill:#566AB2;}
</style>
<path class="st0" d="M191.3,123.7c-2.4,1-4.9,1.8-7.2,1.9c-3.6,0.2-7.6-1.3-9.7-3.1c-3.3-2.8-5.7-4.4-6.7-9.2
c-0.4-2.1-0.2-5.3,0.2-7.2c0.9-4-0.1-6.5-2.9-8.9c-2.3-1.9-5.2-2.4-8.4-2.4s-2.3-0.5-3.1-1c-1.3-0.7-2.4-2.3-1.4-4.4
c0.3-0.7,2-2.3,2.3-2.5c4.3-2.5,9.4-1.7,14,0.2c4.3,1.7,7.5,5,12.2,9.5c4.8,5.5,5.6,7,8.4,11.1c2.1,3.2,4.1,6.6,5.4,10.4
C195.2,120.5,194.2,122.4,191.3,123.7L191.3,123.7z M153.4,104.3c0-2.1,1.7-3.7,3.8-3.7s0.9,0.1,1.3,0.2c0.5,0.2,1,0.5,1.4,0.9
c0.7,0.7,1.1,1.6,1.1,2.6c0,2.1-1.7,3.8-3.8,3.8s-3.7-1.7-3.7-3.8H153.4z M141.2,182.8c-25.5-20-37.8-26.6-42.9-26.3
c-4.8,0.3-3.9,5.7-2.8,9.3c1.1,3.5,2.5,5.9,4.5,9c1.4,2,2.3,5.1-1.4,7.3c-8.2,5.1-22.5-1.7-23.1-2c-16.6-9.8-30.5-22.7-40.2-40.3
c-9.5-17-14.9-35.2-15.8-54.6c-0.2-4.7,1.1-6.4,5.8-7.2c6.2-1.1,12.5-1.4,18.7-0.5c26,3.8,48.1,15.4,66.7,33.8
c10.6,10.5,18.6,23,26.8,35.2c8.8,13,18.2,25.4,30.2,35.5c4.3,3.6,7.6,6.3,10.9,8.2c-9.8,1.1-26.1,1.3-37.2-7.5L141.2,182.8z
M289.5,18c-3.1-1.5-4.4,1.4-6.3,2.8c-0.6,0.5-1.1,1.1-1.7,1.7c-4.5,4.8-9.8,8-16.8,7.6c-10.1-0.6-18.7,2.6-26.4,10.4
c-1.6-9.5-7-15.2-15.2-18.9c-4.3-1.9-8.6-3.8-11.6-7.9c-2.1-2.9-2.7-6.2-3.7-9.4c-0.7-2-1.3-3.9-3.6-4.3c-2.4-0.4-3.4,1.7-4.3,3.4
c-3.8,7-5.3,14.6-5.2,22.4c0.3,17.5,7.7,31.5,22.4,41.4c1.7,1.1,2.1,2.3,1.6,3.9c-1,3.4-2.2,6.7-3.3,10.1c-0.7,2.2-1.7,2.7-4,1.7
c-8.1-3.4-15-8.4-21.2-14.4c-10.4-10.1-19.9-21.2-31.6-30c-2.8-2.1-5.5-4-8.4-5.7c-12-11.7,1.6-21.3,4.7-22.4
c3.3-1.2,1.2-5.3-9.5-5.2c-10.6,0-20.3,3.6-32.8,8.4c-1.8,0.7-3.7,1.2-5.7,1.7c-11.3-2.1-22.9-2.6-35.1-1.2
c-23,2.5-41.4,13.4-54.8,32C1,68.3-2.8,93.6,1.9,120c4.9,27.8,19.1,50.9,41,68.9c22.6,18.7,48.7,27.8,78.5,26.1
c18.1-1,38.2-3.5,60.9-22.7c5.7,2.8,11.7,4,21.7,4.8c7.7,0.7,15.1-0.4,20.8-1.5c9-1.9,8.4-10.2,5.1-11.7
c-26.3-12.3-20.5-7.3-25.7-11.3c13.3-15.8,33.5-32.2,41.3-85.4c0.6-4.2,0.1-6.9,0-10.3c0-2.1,0.4-2.9,2.8-3.1
c6.6-0.8,13-2.6,18.8-5.8c17-9.3,23.9-24.6,25.5-42.9c0.2-2.8,0-5.7-3-7.2L289.5,18z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,16 +1,16 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 26.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="10.5862" y1="1.61" x2="36.0543" y2="44.1206">
<stop offset="0.002" style="stop-color:#9C55D4"/>
<stop offset="0.003" style="stop-color:#20808D"/>
<stop offset="0.3731" style="stop-color:#218F9B"/>
<stop offset="1" style="stop-color:#22B1BC"/>
</linearGradient>
<path style="fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_1_);" d="M11.469,4l11.39,10.494v-0.002V4.024h2.217v10.517
L36.518,4v11.965h4.697v17.258h-4.683v10.654L25.077,33.813v10.18h-2.217V33.979L11.482,44V33.224H6.785V15.965h4.685V4z
M21.188,18.155H9.002v12.878h2.477v-4.062L21.188,18.155z M13.699,27.943v11.17l9.16-8.068V19.623L13.699,27.943z M25.141,30.938
V19.612l9.163,8.321v5.291h0.012v5.775L25.141,30.938z M36.532,31.033h2.466V18.155H26.903l9.629,8.725V31.033z M34.301,15.965
V9.038l-7.519,6.927H34.301z M21.205,15.965h-7.519V9.038L21.205,15.965z"/>
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 26.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="10.5862" y1="1.61" x2="36.0543" y2="44.1206">
<stop offset="0.002" style="stop-color:#9C55D4"/>
<stop offset="0.003" style="stop-color:#20808D"/>
<stop offset="0.3731" style="stop-color:#218F9B"/>
<stop offset="1" style="stop-color:#22B1BC"/>
</linearGradient>
<path style="fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_1_);" d="M11.469,4l11.39,10.494v-0.002V4.024h2.217v10.517
L36.518,4v11.965h4.697v17.258h-4.683v10.654L25.077,33.813v10.18h-2.217V33.979L11.482,44V33.224H6.785V15.965h4.685V4z
M21.188,18.155H9.002v12.878h2.477v-4.062L21.188,18.155z M13.699,27.943v11.17l9.16-8.068V19.623L13.699,27.943z M25.141,30.938
V19.612l9.163,8.321v5.291h0.012v5.775L25.141,30.938z M36.532,31.033h2.466V18.155H26.903l9.629,8.725V31.033z M34.301,15.965
V9.038l-7.519,6.927H34.301z M21.205,15.965h-7.519V9.038L21.205,15.965z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

View file

@ -2,6 +2,6 @@
3:I[15102,["665","static/chunks/3014691f-b7b79b78e27792f3.js","990","static/chunks/13b76428-ebdf3012af0e4489.js","402","static/chunks/402-d418d0a7bf158a22.js","313","static/chunks/313-fe5a1ed341fdff45.js","899","static/chunks/899-3dc1e1a0832876e3.js","539","static/chunks/539-a5e6699b98b26b32.js","250","static/chunks/250-b776bd9ac8911291.js","699","static/chunks/699-2e0b76ba9cd1d301.js","931","static/chunks/app/page-4f83db2427a4fe55.js"],"default",1]
4:I[4707,[],""]
5:I[36423,[],""]
0:["EUsvrfLmgLy71o8GGPdmU",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/css/31b7f215e119031e.css","precedence":"next","crossOrigin":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/css/c1c21001170a99e0.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_b0dd8a","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]]
0:["EUsvrfLmgLy71o8GGPdmU",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/31b7f215e119031e.css","precedence":"next","crossOrigin":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/c1c21001170a99e0.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_b0dd8a","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]]
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"LiteLLM Dashboard"}],["$","meta","3",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","link","5",{"rel":"icon","href":"./favicon.ico"}],["$","meta","6",{"name":"next-size-adjust"}]]
1:null

View file

@ -2,6 +2,6 @@
3:I[52829,["402","static/chunks/402-d418d0a7bf158a22.js","313","static/chunks/313-fe5a1ed341fdff45.js","250","static/chunks/250-b776bd9ac8911291.js","699","static/chunks/699-2e0b76ba9cd1d301.js","418","static/chunks/app/model_hub/page-ce40c5a05f3174ca.js"],"default",1]
4:I[4707,[],""]
5:I[36423,[],""]
0:["EUsvrfLmgLy71o8GGPdmU",[[["",{"children":["model_hub",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["model_hub",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","model_hub","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/css/31b7f215e119031e.css","precedence":"next","crossOrigin":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/css/c1c21001170a99e0.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_b0dd8a","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]]
0:["EUsvrfLmgLy71o8GGPdmU",[[["",{"children":["model_hub",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["model_hub",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","model_hub","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/31b7f215e119031e.css","precedence":"next","crossOrigin":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/c1c21001170a99e0.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_b0dd8a","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]]
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"LiteLLM Dashboard"}],["$","meta","3",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","link","5",{"rel":"icon","href":"./favicon.ico"}],["$","meta","6",{"name":"next-size-adjust"}]]
1:null

File diff suppressed because one or more lines are too long

View file

@ -2,6 +2,6 @@
3:I[12011,["665","static/chunks/3014691f-b7b79b78e27792f3.js","402","static/chunks/402-d418d0a7bf158a22.js","899","static/chunks/899-3dc1e1a0832876e3.js","250","static/chunks/250-b776bd9ac8911291.js","461","static/chunks/app/onboarding/page-9659af55efbf16f8.js"],"default",1]
4:I[4707,[],""]
5:I[36423,[],""]
0:["EUsvrfLmgLy71o8GGPdmU",[[["",{"children":["onboarding",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["onboarding",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","onboarding","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/css/31b7f215e119031e.css","precedence":"next","crossOrigin":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/css/c1c21001170a99e0.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_b0dd8a","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]]
0:["EUsvrfLmgLy71o8GGPdmU",[[["",{"children":["onboarding",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["onboarding",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","onboarding","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/31b7f215e119031e.css","precedence":"next","crossOrigin":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/c1c21001170a99e0.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_b0dd8a","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]]
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"LiteLLM Dashboard"}],["$","meta","3",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","link","5",{"rel":"icon","href":"./favicon.ico"}],["$","meta","6",{"name":"next-size-adjust"}]]
1:null

View file

@ -25,6 +25,7 @@ from litellm.types.mcp import (
MCPTransportType,
)
from litellm.types.router import RouterErrors, UpdateRouterConfig
from litellm.types.secret_managers.main import KeyManagementSystem
from litellm.types.utils import (
CallTypes,
EmbeddingResponse,
@ -185,27 +186,6 @@ def hash_token(token: str):
return hashed_token
class LiteLLM_UpperboundKeyGenerateParams(LiteLLMPydanticObjectBase):
"""
Set default upperbound to max budget a key called via `/key/generate` can be.
Args:
max_budget (Optional[float], optional): Max budget a key can be. Defaults to None.
budget_duration (Optional[str], optional): Duration of the budget. Defaults to None.
duration (Optional[str], optional): Duration of the key. Defaults to None.
max_parallel_requests (Optional[int], optional): Max number of requests that can be made in parallel. Defaults to None.
tpm_limit (Optional[int], optional): Tpm limit. Defaults to None.
rpm_limit (Optional[int], optional): Rpm limit. Defaults to None.
"""
max_budget: Optional[float] = None
budget_duration: Optional[str] = None
duration: Optional[str] = None
max_parallel_requests: Optional[int] = None
tpm_limit: Optional[int] = None
rpm_limit: Optional[int] = None
class KeyManagementRoutes(str, enum.Enum):
"""
Enum for key management routes
@ -1398,40 +1378,6 @@ class DeleteOrganizationRequest(LiteLLMPydanticObjectBase):
organization_ids: List[str] # required
class KeyManagementSystem(enum.Enum):
GOOGLE_KMS = "google_kms"
AZURE_KEY_VAULT = "azure_key_vault"
AWS_SECRET_MANAGER = "aws_secret_manager"
GOOGLE_SECRET_MANAGER = "google_secret_manager"
HASHICORP_VAULT = "hashicorp_vault"
LOCAL = "local"
AWS_KMS = "aws_kms"
class KeyManagementSettings(LiteLLMPydanticObjectBase):
hosted_keys: Optional[List] = None
store_virtual_keys: Optional[bool] = False
"""
If True, virtual keys created by litellm will be stored in the secret manager
"""
prefix_for_stored_virtual_keys: str = "litellm/"
"""
If set, this prefix will be used for stored virtual keys in the secret manager
"""
access_mode: Literal["read_only", "write_only", "read_and_write"] = "read_only"
"""
Access mode for the secret manager, when write_only will only use for writing secrets
"""
primary_secret_name: Optional[str] = None
"""
If set, will read secrets from this primary secret in the secret manager
eg. on AWS you can store multiple secret values as K/V pairs in a single secret
"""
class TeamDefaultSettings(LiteLLMPydanticObjectBase):
team_id: str

View file

@ -947,6 +947,7 @@ def team_member_add_duplication_check(
This check is done BEFORE we create/fetch the user, so it only prevents
obvious duplicates where both user_id and user_email match exactly.
"""
def _check_member_duplication(member: Member):
# Check by user_id if provided
if member.user_id is not None:
@ -958,7 +959,7 @@ def team_member_add_duplication_check(
param="user_id",
code="400",
)
# Check by user_email if provided
if member.user_email is not None:
for existing_member in existing_team_row.members_with_roles:
@ -1014,13 +1015,13 @@ async def _process_team_members(
"""Process and add new team members."""
updated_users: List[LiteLLM_UserTable] = []
updated_team_memberships: List[LiteLLM_TeamMembership] = []
default_team_budget_id = (
complete_team_data.metadata.get("team_member_budget_id")
if complete_team_data.metadata is not None
else None
)
if isinstance(data.member, Member):
try:
updated_user, updated_tm = await add_new_member(
@ -1068,7 +1069,7 @@ async def _process_team_members(
updated_users.append(updated_user)
if updated_tm is not None:
updated_team_memberships.append(updated_tm)
return updated_users, updated_team_memberships
@ -1080,7 +1081,7 @@ async def _update_team_members_list(
"""Update the team's members_with_roles list."""
if isinstance(data.member, Member):
new_member = data.member.model_copy()
# get user id
if new_member.user_id is None and new_member.user_email is not None:
for user in updated_users:
@ -1089,33 +1090,42 @@ async def _update_team_members_list(
and user.user_email == new_member.user_email
):
new_member.user_id = user.user_id
# Check if member already exists in team before adding
member_already_exists = False
for existing_member in complete_team_data.members_with_roles:
if (new_member.user_id is not None and existing_member.user_id == new_member.user_id) or \
(new_member.user_email is not None and existing_member.user_email == new_member.user_email):
if (
new_member.user_id is not None
and existing_member.user_id == new_member.user_id
) or (
new_member.user_email is not None
and existing_member.user_email == new_member.user_email
):
member_already_exists = True
break
if not member_already_exists:
complete_team_data.members_with_roles.append(new_member)
elif isinstance(data.member, List):
for nm in data.member:
if nm.user_id is None and nm.user_email is not None:
for user in updated_users:
if user.user_email is not None and user.user_email == nm.user_email:
nm.user_id = user.user_id
# Check if member already exists in team before adding
member_already_exists = False
for existing_member in complete_team_data.members_with_roles:
if (nm.user_id is not None and existing_member.user_id == nm.user_id) or \
(nm.user_email is not None and existing_member.user_email == nm.user_email):
if (
nm.user_id is not None and existing_member.user_id == nm.user_id
) or (
nm.user_email is not None
and existing_member.user_email == nm.user_email
):
member_already_exists = True
break
if not member_already_exists:
complete_team_data.members_with_roles.append(nm)
@ -2174,30 +2184,15 @@ async def list_team(
filtered_response = []
if user_id:
# Get user object to access their teams array
try:
user_object = await prisma_client.db.litellm_usertable.find_unique(
where={"user_id": user_id}
)
if user_object and user_object.teams:
# Filter teams based on user's teams array
for team in response:
if team.team_id in user_object.teams:
for team in response:
if team.members_with_roles:
for member in team.members_with_roles:
if (
"user_id" in member
and member["user_id"] is not None
and member["user_id"] == user_id
):
filtered_response.append(team)
except Exception as e:
verbose_proxy_logger.debug(
f"Error fetching user for team filtering: {str(e)}"
)
# Fall back to checking members_with_roles if user lookup fails
for team in response:
if team.members_with_roles:
for member in team.members_with_roles:
if (
"user_id" in member
and member["user_id"] is not None
and member["user_id"] == user_id
):
filtered_response.append(team)
else:
filtered_response = response
@ -2396,7 +2391,7 @@ def add_new_models_to_team(
): # implies all model access
current_models = [SpecialModelNames.all_proxy_models.value]
else:
current_models = []
current_models = team_obj.models
updated_models = list(set(current_models + new_models))
return updated_models

View file

@ -353,10 +353,18 @@ from litellm.types.llms.anthropic import (
AnthropicResponseUsageBlock,
)
from litellm.types.llms.openai import HttpxBinaryResponseContent
from litellm.types.proxy.management_endpoints.ui_sso import (
DefaultTeamSSOParams,
LiteLLM_UpperboundKeyGenerateParams,
)
from litellm.types.router import DeploymentTypedDict
from litellm.types.router import ModelInfo as RouterModelInfo
from litellm.types.router import RouterGeneralSettings, updateDeployment
from litellm.types.scheduler import DefaultPriorities
from litellm.types.secret_managers.main import (
KeyManagementSettings,
KeyManagementSystem,
)
from litellm.types.utils import CredentialItem, CustomHuggingfaceTokenizer
from litellm.types.utils import ModelInfo as ModelMapInfo
from litellm.types.utils import RawRequestTypedDict, StandardLoggingPayload
@ -759,46 +767,49 @@ try:
current_dir = os.path.dirname(os.path.abspath(__file__))
ui_path = os.path.join(current_dir, "_experimental", "out")
litellm_asset_prefix = "/litellm-asset-prefix"
# Iterate through files in the UI directory
for root, dirs, files in os.walk(ui_path):
for filename in files:
file_path = os.path.join(root, filename)
# Skip binary files and files that don't need path replacement
if filename.endswith(
(
".png",
".jpg",
".jpeg",
".gif",
".ico",
".woff",
".woff2",
".ttf",
".eot",
)
):
continue
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# Replace the asset prefix with the server root path
modified_content = content.replace(
f"{litellm_asset_prefix}",
f"{server_root_path}",
)
# Only modify files if a custom server root path is set
if server_root_path and server_root_path != "/":
# Iterate through files in the UI directory
for root, dirs, files in os.walk(ui_path):
for filename in files:
file_path = os.path.join(root, filename)
# Skip binary files and files that don't need path replacement
if filename.endswith(
(
".png",
".jpg",
".jpeg",
".gif",
".ico",
".woff",
".woff2",
".ttf",
".eot",
)
):
continue
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# Replace the /.well-known/litellm-ui-config with the server root path
modified_content = modified_content.replace(
"/litellm/.well-known/litellm-ui-config",
f"{server_root_path}/.well-known/litellm-ui-config",
)
# Replace the asset prefix with the server root path
modified_content = content.replace(
f"{litellm_asset_prefix}",
f"{server_root_path}",
)
with open(file_path, "w", encoding="utf-8") as f:
f.write(modified_content)
except UnicodeDecodeError:
# Skip binary files that can't be decoded
continue
# Replace the /.well-known/litellm-ui-config with the server root path
modified_content = modified_content.replace(
"/litellm/.well-known/litellm-ui-config",
f"{server_root_path}/.well-known/litellm-ui-config",
)
with open(file_path, "w", encoding="utf-8") as f:
f.write(modified_content)
except UnicodeDecodeError:
# Skip binary files that can't be decoded
continue
# # Mount the _next directory at the root level
app.mount(
@ -5600,7 +5611,9 @@ def _add_team_models_to_all_models(
team_models.setdefault(model_id, set()).add(team_object.team_id)
else:
for model_name in team_object.models:
_models = llm_router.get_model_list(model_name=model_name)
_models = llm_router.get_model_list(
model_name=model_name, team_id=team_object.team_id
)
if _models is not None:
for model in _models:
model_id = model.get("model_info", {}).get("id", None)
@ -5636,6 +5649,7 @@ async def get_all_team_models(
team_db_objects = await prisma_client.db.litellm_teamtable.find_many(
where={"team_id": {"in": user_teams}}
)
team_db_objects_typed = [
LiteLLM_TeamTable(**team_db_object.model_dump())
for team_db_object in team_db_objects
@ -5706,6 +5720,7 @@ async def get_all_team_and_direct_access_models(
prisma_client=prisma_client,
llm_router=llm_router,
)
for _model in all_models:
model_id = _model.get("model_info", {}).get("id", None)
team_only_model_id = _model.get("model_info", {}).get("team_id", None)
@ -5721,9 +5736,11 @@ async def get_all_team_and_direct_access_models(
)
## ADD DIRECT_ACCESS TO RELEVANT MODELS
for _model in all_models:
model_id = _model.get("model_info", {}).get("id", None)
if model_id is not None and model_id in direct_access_models:
_model["model_info"]["direct_access"] = True
## FILTER OUT MODELS THAT ARE NOT IN DIRECT_ACCESS_MODELS OR ACCESS_VIA_TEAM_IDS - only show user models they can call
@ -7515,6 +7532,7 @@ async def new_invitation(
from litellm.proxy.management_helpers.user_invitation import (
create_invitation_for_user,
)
global prisma_client
if prisma_client is None:
@ -7533,7 +7551,7 @@ async def new_invitation(
)
},
)
response = await create_invitation_for_user(
data=data,
user_api_key_dict=user_api_key_dict,
@ -7543,7 +7561,6 @@ async def new_invitation(
raise handle_exception_on_proxy(e)
@router.get(
"/invitation/info",
tags=["Invite Links"],

View file

@ -3643,7 +3643,10 @@ class Router:
litellm.ContentPolicyViolationError: when `mock_testing_content_policy_fallbacks=True` passed in request params
"""
mock_testing_params = MockRouterTestingParams.from_kwargs(kwargs)
if mock_testing_params.mock_testing_fallbacks is not None and mock_testing_params.mock_testing_fallbacks is True:
if (
mock_testing_params.mock_testing_fallbacks is not None
and mock_testing_params.mock_testing_fallbacks is True
):
raise litellm.InternalServerError(
model=model_group,
llm_provider="",
@ -5584,8 +5587,27 @@ class Router:
return model["model_name"]
return None
def should_include_deployment(
self, model_name: str, model: dict, team_id: Optional[str] = None
) -> bool:
"""
Get the team-specific model name if team_id matches the deployment.
"""
if (
team_id is not None
and model["model_info"].get("team_id") == team_id
and model_name == model["model_info"].get("team_public_model_name")
):
return True
elif model_name is not None and model["model_name"] == model_name:
return True
return False
def _get_all_deployments(
self, model_name: str, model_alias: Optional[str] = None
self,
model_name: str,
model_alias: Optional[str] = None,
team_id: Optional[str] = None,
) -> List[DeploymentTypedDict]:
"""
Return all deployments of a model name
@ -5594,7 +5616,9 @@ class Router:
"""
returned_models: List[DeploymentTypedDict] = []
for model in self.model_list:
if model_name is not None and model["model_name"] == model_name:
if self.should_include_deployment(
model_name=model_name, model=model, team_id=team_id
):
if model_alias is not None:
alias_model = copy.deepcopy(model)
alias_model["model_name"] = model_alias
@ -5692,16 +5716,20 @@ class Router:
return returned_models
def get_model_list(
self, model_name: Optional[str] = None
self, model_name: Optional[str] = None, team_id: Optional[str] = None
) -> Optional[List[DeploymentTypedDict]]:
"""
Includes router model_group_alias'es as well
if team_id specified, returns matching team-specific models
"""
if hasattr(self, "model_list"):
returned_models: List[DeploymentTypedDict] = []
if model_name is not None:
returned_models.extend(self._get_all_deployments(model_name=model_name))
returned_models.extend(
self._get_all_deployments(model_name=model_name, team_id=team_id)
)
if hasattr(self, "model_group_alias"):
returned_models.extend(

View file

@ -11,10 +11,10 @@ import litellm
from litellm._logging import print_verbose, verbose_logger
from litellm.caching.caching import DualCache
from litellm.llms.custom_httpx.http_handler import HTTPHandler
from litellm.proxy._types import KeyManagementSystem
from litellm.secret_managers.get_azure_ad_token_provider import (
get_azure_ad_token_provider,
)
from litellm.types.secret_managers.main import KeyManagementSystem
oidc_cache = DualCache()

View file

@ -1,4 +1,27 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
class LiteLLMPydanticObjectBase(BaseModel):
"""
Implements default functions, all pydantic objects should have.
"""
def json(self, **kwargs): # type: ignore
try:
return self.model_dump(**kwargs) # noqa
except Exception:
# if using pydantic v1
return self.dict(**kwargs)
def fields_set(self):
try:
return self.model_fields_set # noqa
except Exception:
# if using pydantic v1
return self.__fields_set__
model_config = ConfigDict(protected_namespaces=())
class BaseLiteLLMOpenAIResponseObject(BaseModel):

View file

@ -2,9 +2,29 @@ from typing import List, Literal, Optional, TypedDict, Union
from pydantic import Field
from litellm.proxy._types import LiteLLMPydanticObjectBase, LitellmUserRoles
from litellm.types.utils import LiteLLMPydanticObjectBase
class LiteLLM_UpperboundKeyGenerateParams(LiteLLMPydanticObjectBase):
"""
Set default upperbound to max budget a key called via `/key/generate` can be.
Args:
max_budget (Optional[float], optional): Max budget a key can be. Defaults to None.
budget_duration (Optional[str], optional): Duration of the budget. Defaults to None.
duration (Optional[str], optional): Duration of the key. Defaults to None.
max_parallel_requests (Optional[int], optional): Max number of requests that can be made in parallel. Defaults to None.
tpm_limit (Optional[int], optional): Tpm limit. Defaults to None.
rpm_limit (Optional[int], optional): Rpm limit. Defaults to None.
"""
max_budget: Optional[float] = None
budget_duration: Optional[str] = None
duration: Optional[str] = None
max_parallel_requests: Optional[int] = None
tpm_limit: Optional[int] = None
rpm_limit: Optional[int] = None
class MicrosoftGraphAPIUserGroupDirectoryObject(TypedDict, total=False):
"""Model for Microsoft Graph API directory object"""

View file

@ -0,0 +1,38 @@
import enum
from typing import List, Literal, Optional
from litellm.types.llms.base import LiteLLMPydanticObjectBase
class KeyManagementSystem(enum.Enum):
GOOGLE_KMS = "google_kms"
AZURE_KEY_VAULT = "azure_key_vault"
AWS_SECRET_MANAGER = "aws_secret_manager"
GOOGLE_SECRET_MANAGER = "google_secret_manager"
HASHICORP_VAULT = "hashicorp_vault"
LOCAL = "local"
AWS_KMS = "aws_kms"
class KeyManagementSettings(LiteLLMPydanticObjectBase):
hosted_keys: Optional[List] = None
store_virtual_keys: Optional[bool] = False
"""
If True, virtual keys created by litellm will be stored in the secret manager
"""
prefix_for_stored_virtual_keys: str = "litellm/"
"""
If set, this prefix will be used for stored virtual keys in the secret manager
"""
access_mode: Literal["read_only", "write_only", "read_and_write"] = "read_only"
"""
Access mode for the secret manager, when write_only will only use for writing secrets
"""
primary_secret_name: Optional[str] = None
"""
If set, will read secrets from this primary secret in the secret manager
eg. on AWS you can store multiple secret values as K/V pairs in a single secret
"""

View file

@ -33,7 +33,10 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator
from typing_extensions import Callable, Dict, Required, TypedDict, override
import litellm
from litellm.types.llms.base import BaseLiteLLMOpenAIResponseObject
from litellm.types.llms.base import (
BaseLiteLLMOpenAIResponseObject,
LiteLLMPydanticObjectBase,
)
from ..litellm_core_utils.core_helpers import map_finish_reason
from .guardrails import GuardrailEventHooks
@ -63,28 +66,6 @@ def _generate_id(): # private helper function
return "chatcmpl-" + str(uuid.uuid4())
class LiteLLMPydanticObjectBase(BaseModel):
"""
Implements default functions, all pydantic objects should have.
"""
def json(self, **kwargs): # type: ignore
try:
return self.model_dump(**kwargs) # noqa
except Exception:
# if using pydantic v1
return self.dict(**kwargs)
def fields_set(self):
try:
return self.model_fields_set # noqa
except Exception:
# if using pydantic v1
return self.__fields_set__
model_config = ConfigDict(protected_namespaces=())
class LiteLLMCommonStrings(Enum):
redacted_by_litellm = "redacted by litellm. 'litellm.turn_off_message_logging=True'"
llm_provider_not_provided = "Unmapped LLM provider for this endpoint. You passed model={model}, custom_llm_provider={custom_llm_provider}. Check supported provider and route: https://docs.litellm.ai/docs/providers"

372
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "litellm"
version = "1.73.6"
version = "1.73.6.rc.2"
description = "Library to easily interface with LLM API providers"
authors = ["BerriAI"]
license = "MIT"
@ -58,7 +58,7 @@ redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.
mcp = {version = "1.9.3", optional = true, python = ">=3.10"}
litellm-proxy-extras = {version = "0.2.6", optional = true}
rich = {version = "13.7.1", optional = true}
litellm-enterprise = {version = "0.1.9", optional = true}
litellm-enterprise = {version = "0.1.10", optional = true}
diskcache = {version = "^5.6.1", optional = true}
[tool.poetry.extras]
@ -141,7 +141,7 @@ requires = ["poetry-core", "wheel"]
build-backend = "poetry.core.masonry.api"
[tool.commitizen]
version = "1.73.6"
version = "1.73.6.rc.2"
version_files = [
"pyproject.toml:^version"
]

View file

@ -57,4 +57,4 @@ websockets==13.1.0 # for realtime API
########################
# LITELLM ENTERPRISE DEPENDENCIES
########################
litellm-enterprise==0.1.9
litellm-enterprise==0.1.10

View file

@ -0,0 +1,98 @@
import ast
import os
import sys
def test_proxy_types_not_imported():
"""
Test that proxy._types is not directly imported in litellm/__init__.py
by examining the source code using AST parsing.
"""
# Read the litellm/__init__.py file
# local_init_file = "../litellm/"
init_file_path = os.path.join("./litellm", "__init__.py")
if not os.path.exists(init_file_path):
raise Exception(f"Could not find {init_file_path}")
with open(init_file_path, "r") as f:
content = f.read()
lines = content.splitlines() # Get lines for line number reporting
try:
tree = ast.parse(content)
except SyntaxError as e:
raise Exception(f"Could not parse {init_file_path}: {e}")
# Check for direct imports of proxy._types
found_imports = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if "proxy._types" in alias.name or "proxy/_types" in alias.name:
line_num = node.lineno
line_content = lines[line_num - 1] if line_num <= len(lines) else "Unknown"
import_statement = f"import {alias.name}"
found_imports.append({
'type': 'import',
'line': line_num,
'content': line_content.strip(),
'statement': import_statement,
'module': alias.name
})
elif isinstance(node, ast.ImportFrom):
if node.module and ("proxy._types" in node.module or "proxy/_types" in node.module):
line_num = node.lineno
line_content = lines[line_num - 1] if line_num <= len(lines) else "Unknown"
import_names = [alias.name for alias in node.names]
import_statement = f"from {node.module} import {', '.join(import_names)}"
found_imports.append({
'type': 'from_import',
'line': line_num,
'content': line_content.strip(),
'statement': import_statement,
'module': node.module
})
if found_imports:
print("❌ BAD, this can import time to import litellm. Found direct imports of proxy._types in litellm/__init__.py:")
print("=" * 80)
for imp in found_imports:
print(f"Line {imp['line']}: {imp['content']}")
print(f" Type: {imp['type']}")
print(f" Statement: {imp['statement']}")
print(f" Module: {imp['module']}")
print("-" * 80)
print("To fix this, please conditionally import this TYPE using TYPE_CHECKING")
raise Exception(
f"Found {len(found_imports)} direct import(s) of proxy._types in litellm/__init__.py"
)
print("✓ No direct imports of proxy._types found in litellm/__init__.py")
return True
def main():
"""
Main function to run the import test
"""
print("=" * 60)
print("Testing litellm import performance")
print("Checking that proxy._types is not directly imported from litellm/__init__.py")
print("=" * 60)
try:
test_proxy_types_not_imported()
print("\n" + "=" * 60)
print("✓ Test passed! proxy._types is not directly imported from litellm/__init__.py")
print("=" * 60)
except Exception as e:
print(f"\n❌ Test failed: {e}")
print("=" * 60)
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -296,7 +296,7 @@ def test_should_read_secret_from_secret_manager():
"""
Test that _should_read_secret_from_secret_manager returns correct values based on access mode
"""
from litellm.proxy._types import KeyManagementSettings
from litellm.types.secret_managers.main import KeyManagementSettings
# Test when secret manager client is None
litellm.secret_manager_client = None
@ -327,7 +327,7 @@ def test_get_secret_with_access_mode():
"""
Test that get_secret respects access mode settings
"""
from litellm.proxy._types import KeyManagementSettings
from litellm.types.secret_managers.main import KeyManagementSettings
# Set up test environment
test_secret_name = "TEST_SECRET_KEY"

View file

@ -2230,3 +2230,25 @@ def test_get_valid_models_from_dynamic_api_key():
valid_models = get_valid_models(custom_llm_provider="anthropic", litellm_params=creds, check_provider_endpoint=True)
assert len(valid_models) > 0
assert "anthropic/claude-3-7-sonnet-20250219" in valid_models
def test_get_whitelisted_models():
"""
Snapshot of all bedrock models as of 12/24/2024.
Enforce any new bedrock chat model to be added as `bedrock_converse` unless explicitly whitelisted.
Create whitelist to prevent naming regressions for older litellm versions.
"""
whitelisted_models = []
for model, info in litellm.model_cost.items():
if info["litellm_provider"] == "bedrock" and info["mode"] == "chat":
whitelisted_models.append(model)
# Write to a local file
with open("whitelisted_bedrock_models.txt", "w") as file:
for model in whitelisted_models:
file.write(f"{model}\n")
print("whitelisted_models written to whitelisted_bedrock_models.txt")

View file

@ -16,10 +16,11 @@ from litellm.llms.vertex_ai.context_caching.transformation import (
import litellm
from litellm import completion
class TestGoogleAIStudioGemini(BaseLLMChatTest):
def get_base_completion_call_args(self) -> dict:
return {"model": "gemini/gemini-2.0-flash"}
def get_base_completion_call_args_with_reasoning_model(self) -> dict:
return {"model": "gemini/gemini-2.5-flash-preview-04-17"}
@ -34,6 +35,7 @@ class TestGoogleAIStudioGemini(BaseLLMChatTest):
def test_url_context(self):
from litellm.utils import supports_url_context
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
litellm.model_cost = litellm.get_model_cost_map(url="")
@ -46,14 +48,22 @@ class TestGoogleAIStudioGemini(BaseLLMChatTest):
response = self.completion_function(
**base_completion_call_args,
messages=[{"role": "user", "content": "Summarize the content of this URL: https://en.wikipedia.org/wiki/Artificial_intelligence"}],
messages=[
{
"role": "user",
"content": "Summarize the content of this URL: https://en.wikipedia.org/wiki/Artificial_intelligence",
}
],
tools=[{"urlContext": {}}],
)
assert response is not None
assert response.model_extra['vertex_ai_url_context_metadata'] is not None, "URL context metadata should be present"
assert (
response.model_extra["vertex_ai_url_context_metadata"] is not None
), "URL context metadata should be present"
print(f"response={response}")
def test_gemini_context_caching_separate_messages():
messages = [
# System Message
@ -111,7 +121,6 @@ def test_gemini_image_generation():
assert response.choices[0].message.content is not None
def test_gemini_thinking():
litellm._turn_on_debug()
from litellm.types.utils import Message, CallTypes
@ -119,10 +128,20 @@ def test_gemini_thinking():
import json
messages = [
{"role": "user", "content": "Explain the concept of Occam's Razor and provide a simple, everyday example"}
{
"role": "user",
"content": "Explain the concept of Occam's Razor and provide a simple, everyday example",
}
]
reasoning_content = "I'm thinking about Occam's Razor."
assistant_message = Message(content='Okay, let\'s break down Occam\'s Razor.', reasoning_content=reasoning_content, role='assistant', tool_calls=None, function_call=None, provider_specific_fields=None)
assistant_message = Message(
content="Okay, let's break down Occam's Razor.",
reasoning_content=reasoning_content,
role="assistant",
tool_calls=None,
function_call=None,
provider_specific_fields=None,
)
messages.append(assistant_message)
@ -131,12 +150,12 @@ def test_gemini_thinking():
kwargs={
"model": "gemini/gemini-2.5-flash-preview-04-17",
"messages": messages,
}
},
)
assert reasoning_content in json.dumps(raw_request)
response = completion(
model="gemini/gemini-2.5-flash-preview-04-17",
messages=messages, # make sure call works
messages=messages, # make sure call works
)
print(response.choices[0].message)
assert response.choices[0].message.content is not None
@ -152,9 +171,14 @@ def test_gemini_thinking_budget_0():
endpoint=CallTypes.completion,
kwargs={
"model": "gemini/gemini-2.5-flash-preview-04-17",
"messages": [{"role": "user", "content": "Explain the concept of Occam's Razor and provide a simple, everyday example"}],
"thinking": {"type": "enabled", "budget_tokens": 0}
}
"messages": [
{
"role": "user",
"content": "Explain the concept of Occam's Razor and provide a simple, everyday example",
}
],
"thinking": {"type": "enabled", "budget_tokens": 0},
},
)
print(raw_request)
assert "0" in json.dumps(raw_request["raw_request_body"])
@ -163,8 +187,13 @@ def test_gemini_thinking_budget_0():
def test_gemini_finish_reason():
import os
from litellm import completion
litellm._turn_on_debug()
response = completion(model="gemini/gemini-1.5-pro", messages=[{"role": "user", "content": "give me 3 random words"}], max_tokens=2)
response = completion(
model="gemini/gemini-1.5-pro",
messages=[{"role": "user", "content": "give me 3 random words"}],
max_tokens=2,
)
print(response)
assert response.choices[0].finish_reason is not None
assert response.choices[0].finish_reason == "length"
@ -172,6 +201,7 @@ def test_gemini_finish_reason():
def test_gemini_url_context():
from litellm import completion
litellm._turn_on_debug()
url = "https://ai.google.dev/gemini-api/docs/models"
@ -180,23 +210,24 @@ def test_gemini_url_context():
{url}
"""
response = completion(
model="gemini/gemini-2.0-flash",
messages=[{"role": "user", "content": prompt}],
tools=[{"urlContext": {}}],
)
model="gemini/gemini-2.0-flash",
messages=[{"role": "user", "content": prompt}],
tools=[{"urlContext": {}}],
)
print(response)
message = response.choices[0].message.content
assert message is not None
url_context_metadata = response.model_extra['vertex_ai_url_context_metadata']
url_context_metadata = response.model_extra["vertex_ai_url_context_metadata"]
assert url_context_metadata is not None
urlMetadata = url_context_metadata[0]['urlMetadata'][0]
assert urlMetadata['retrievedUrl'] == url
assert urlMetadata['urlRetrievalStatus'] == 'URL_RETRIEVAL_STATUS_SUCCESS'
urlMetadata = url_context_metadata[0]["urlMetadata"][0]
assert urlMetadata["retrievedUrl"] == url
assert urlMetadata["urlRetrievalStatus"] == "URL_RETRIEVAL_STATUS_SUCCESS"
@pytest.mark.flaky(retries=3, delay=2)
def test_gemini_with_grounding():
from litellm import completion, Usage, stream_chunk_builder
litellm._turn_on_debug()
litellm.set_verbose = True
tools = [{"googleSearch": {}}]
@ -207,10 +238,15 @@ def test_gemini_with_grounding():
# assert usage.prompt_tokens_details.web_search_requests is not None
# assert usage.prompt_tokens_details.web_search_requests > 0
## Check streaming
response = completion(model="gemini/gemini-2.0-flash", messages=[{"role": "user", "content": "What is the capital of France?"}], tools=tools, stream=True, stream_options={"include_usage": True})
response = completion(
model="gemini/gemini-2.0-flash",
messages=[{"role": "user", "content": "What is the capital of France?"}],
tools=tools,
stream=True,
stream_options={"include_usage": True},
)
chunks = []
for chunk in response:
chunks.append(chunk)
@ -226,6 +262,7 @@ def test_gemini_with_grounding():
def test_gemini_with_empty_function_call_arguments():
from litellm import completion
litellm._turn_on_debug()
tools = [
{
@ -236,6 +273,157 @@ def test_gemini_with_empty_function_call_arguments():
},
}
]
response = completion(model="gemini/gemini-2.0-flash", messages=[{"role": "user", "content": "What is the capital of France?"}], tools=tools)
response = completion(
model="gemini/gemini-2.0-flash",
messages=[{"role": "user", "content": "What is the capital of France?"}],
tools=tools,
)
print(response)
assert response.choices[0].message.content is not None
assert response.choices[0].message.content is not None
@pytest.mark.asyncio
async def test_claude_tool_use_with_gemini():
response = await litellm.anthropic.messages.acreate(
messages=[
{"role": "user", "content": "Hello, can you tell me the weather in Boston?"}
],
model="gemini/gemini-2.5-flash",
stream=True,
max_tokens=100,
tools=[
{
"name": "get_weather",
"description": "Get current weather information for a specific location",
"input_schema": {
"type": "object",
"properties": {"location": {"type": "string"}},
},
}
],
)
is_content_block_tool_use = False
is_partial_json = False
has_usage_in_message_delta = False
is_content_block_stop = False
async for chunk in response:
print(chunk)
if "content_block_stop" in str(chunk):
is_content_block_stop = True
# Handle bytes chunks (SSE format)
if isinstance(chunk, bytes):
chunk_str = chunk.decode("utf-8")
# Parse SSE format: event: <type>\ndata: <json>\n\n
if "data: " in chunk_str:
try:
# Extract JSON from data line
data_line = [
line
for line in chunk_str.split("\n")
if line.startswith("data: ")
][0]
json_str = data_line[6:] # Remove 'data: ' prefix
chunk_data = json.loads(json_str)
# Check for tool_use
if "tool_use" in json_str:
is_content_block_tool_use = True
if "partial_json" in json_str:
is_partial_json = True
if "content_block_stop" in json_str:
is_content_block_stop = True
# Check for usage in message_delta with stop_reason
if (
chunk_data.get("type") == "message_delta"
and chunk_data.get("delta", {}).get("stop_reason") is not None
and "usage" in chunk_data
):
has_usage_in_message_delta = True
# Verify usage has the expected structure
usage = chunk_data["usage"]
assert (
"input_tokens" in usage
), "input_tokens should be present in usage"
assert (
"output_tokens" in usage
), "output_tokens should be present in usage"
assert isinstance(
usage["input_tokens"], int
), "input_tokens should be an integer"
assert isinstance(
usage["output_tokens"], int
), "output_tokens should be an integer"
print(f"Found usage in message_delta: {usage}")
except (json.JSONDecodeError, IndexError) as e:
# Skip chunks that aren't valid JSON
pass
else:
# Handle dict chunks (fallback)
if "tool_use" in str(chunk):
is_content_block_tool_use = True
if "partial_json" in str(chunk):
is_partial_json = True
if "content_block_stop" in str(chunk):
is_content_block_stop = True
assert is_content_block_tool_use, "content_block_tool_use should be present"
assert is_partial_json, "partial_json should be present"
assert (
has_usage_in_message_delta
), "Usage should be present in message_delta with stop_reason"
assert is_content_block_stop, "is_content_block_stop should be present"
def test_gemini_tool_use():
data = {
"max_tokens": 8192,
"stream": True,
"temperature": 0.3,
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What's the weather like in Lima, Peru today?"},
],
"model": "gemini/gemini-2.0-flash",
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Retrieve current weather for a specific location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and country, e.g., Lima, Peru",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit",
},
},
"required": ["location"],
},
},
}
],
"stream_options": {"include_usage": True},
}
response = litellm.completion(**data)
print(response)
stop_reason = None
for chunk in response:
print(chunk)
if chunk.choices[0].finish_reason:
stop_reason = chunk.choices[0].finish_reason
assert stop_reason is not None
assert stop_reason == "tool_calls"

View file

@ -158,26 +158,6 @@ def test_get_model_info_ft_model_with_provider_prefix():
assert info["key"] == "ft:gpt-3.5-turbo"
def test_get_whitelisted_models():
"""
Snapshot of all bedrock models as of 12/24/2024.
Enforce any new bedrock chat model to be added as `bedrock_converse` unless explicitly whitelisted.
Create whitelist to prevent naming regressions for older litellm versions.
"""
whitelisted_models = []
for model, info in litellm.model_cost.items():
if info["litellm_provider"] == "bedrock" and info["mode"] == "chat":
whitelisted_models.append(model)
# Write to a local file
with open("whitelisted_bedrock_models.txt", "w") as file:
for model in whitelisted_models:
file.write(f"{model}\n")
print("whitelisted_models written to whitelisted_bedrock_models.txt")
def _enforce_bedrock_converse_models(
model_cost: List[Dict[str, Any]], whitelist_models: List[str]

View file

@ -80,7 +80,6 @@ from litellm.proxy._types import (
DynamoDBArgs,
GenerateKeyRequest,
KeyRequest,
LiteLLM_UpperboundKeyGenerateParams,
NewCustomerRequest,
NewTeamRequest,
NewUserRequest,
@ -92,6 +91,7 @@ from litellm.proxy._types import (
UpdateUserRequest,
UserAPIKeyAuth,
)
from litellm.types.proxy.management_endpoints.ui_sso import LiteLLM_UpperboundKeyGenerateParams
proxy_logging_obj = ProxyLogging(user_api_key_cache=DualCache())

View file

@ -87,12 +87,12 @@ verbose_proxy_logger.setLevel(level=logging.DEBUG)
from starlette.datastructures import URL
from litellm.caching.caching import DualCache
from litellm.types.proxy.management_endpoints.ui_sso import LiteLLM_UpperboundKeyGenerateParams
from litellm.proxy._types import (
DynamoDBArgs,
GenerateKeyRequest,
RegenerateKeyRequest,
KeyRequest,
LiteLLM_UpperboundKeyGenerateParams,
NewCustomerRequest,
NewTeamRequest,
NewUserRequest,

View file

@ -75,11 +75,11 @@ verbose_proxy_logger.setLevel(level=logging.DEBUG)
from starlette.datastructures import URL
from litellm.caching.caching import DualCache, RedisCache
from litellm.types.proxy.management_endpoints.ui_sso import LiteLLM_UpperboundKeyGenerateParams
from litellm.proxy._types import (
DynamoDBArgs,
GenerateKeyRequest,
KeyRequest,
LiteLLM_UpperboundKeyGenerateParams,
NewCustomerRequest,
NewTeamRequest,
NewUserRequest,

View file

@ -94,11 +94,11 @@ verbose_proxy_logger.setLevel(level=logging.DEBUG)
from starlette.datastructures import URL
from litellm.caching.caching import DualCache
from litellm.types.proxy.management_endpoints.ui_sso import LiteLLM_UpperboundKeyGenerateParams
from litellm.proxy._types import (
DynamoDBArgs,
GenerateKeyRequest,
KeyRequest,
LiteLLM_UpperboundKeyGenerateParams,
NewCustomerRequest,
NewTeamRequest,
NewUserRequest,
@ -3608,7 +3608,7 @@ async def test_key_generate_with_secret_manager_call(prisma_client):
assert it is deleted from the secret manager
"""
from litellm.secret_managers.aws_secret_manager_v2 import AWSSecretsManagerV2
from litellm.proxy._types import KeyManagementSystem, KeyManagementSettings
from litellm.types.secret_managers.main import KeyManagementSystem, KeyManagementSettings
from litellm.proxy.hooks.key_management_event_hooks import (
LITELLM_PREFIX_STORED_VIRTUAL_KEYS,

View file

@ -140,3 +140,35 @@ def test_generic_cost_per_token_above_200k_tokens():
* usage.completion_tokens,
10,
)
def test_generic_cost_per_token_anthropic_prompt_caching():
model = "claude-sonnet-4@20250514"
usage = Usage(
completion_tokens=90,
prompt_tokens=28436,
total_tokens=28526,
completion_tokens_details=CompletionTokensDetailsWrapper(
accepted_prediction_tokens=None,
audio_tokens=None,
reasoning_tokens=0,
rejected_prediction_tokens=None,
text_tokens=None,
),
prompt_tokens_details=PromptTokensDetailsWrapper(
audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None
),
cache_creation_input_tokens=118,
cache_read_input_tokens=28432,
)
custom_llm_provider = "vertex_ai"
prompt_cost, completion_cost = generic_cost_per_token(
model=model,
usage=usage,
custom_llm_provider=custom_llm_provider,
)
print(f"prompt_cost: {prompt_cost}")
assert prompt_cost < 0.085

View file

@ -180,6 +180,101 @@ def test_get_request_tags():
assert "User-Agent: litellm/0.1.0" in tags
def test_get_extra_header_tags():
"""Test the _get_extra_header_tags method with various scenarios."""
import litellm
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
# Store original value to restore later
original_extra_headers = getattr(litellm, "extra_spend_tag_headers", None)
try:
# Test case 1: No extra headers configured
litellm.extra_spend_tag_headers = None
result = StandardLoggingPayloadSetup._get_extra_header_tags(
proxy_server_request={"headers": {"x-custom": "value"}}
)
assert result is None
# Test case 2: Empty extra headers list
litellm.extra_spend_tag_headers = []
result = StandardLoggingPayloadSetup._get_extra_header_tags(
proxy_server_request={"headers": {"x-custom": "value"}}
)
assert result is None
# Test case 3: Extra headers configured but request has no headers dict
litellm.extra_spend_tag_headers = ["x-custom", "x-tenant"]
result = StandardLoggingPayloadSetup._get_extra_header_tags(
proxy_server_request={"headers": "not-a-dict"}
)
assert result is None
# Test case 4: Extra headers configured but none match request headers
litellm.extra_spend_tag_headers = ["x-custom", "x-tenant"]
result = StandardLoggingPayloadSetup._get_extra_header_tags(
proxy_server_request={
"headers": {
"content-type": "application/json",
"authorization": "Bearer token",
}
}
)
assert result is None
# Test case 5: Some extra headers match request headers
litellm.extra_spend_tag_headers = ["x-custom", "x-tenant", "x-missing"]
result = StandardLoggingPayloadSetup._get_extra_header_tags(
proxy_server_request={
"headers": {
"x-custom": "my-custom-value",
"x-tenant": "tenant-123",
"content-type": "application/json",
}
}
)
assert result is not None
assert len(result) == 2
assert "x-custom: my-custom-value" in result
assert "x-tenant: tenant-123" in result
assert "x-missing: " not in str(result)
# Test case 6: All extra headers match request headers
litellm.extra_spend_tag_headers = ["x-custom", "x-tenant"]
result = StandardLoggingPayloadSetup._get_extra_header_tags(
proxy_server_request={
"headers": {
"x-custom": "my-custom-value",
"x-tenant": "tenant-123",
"content-type": "application/json",
}
}
)
assert result is not None
assert len(result) == 2
assert "x-custom: my-custom-value" in result
assert "x-tenant: tenant-123" in result
# Test case 7: Headers with empty values should not be included
litellm.extra_spend_tag_headers = ["x-custom", "x-empty"]
result = StandardLoggingPayloadSetup._get_extra_header_tags(
proxy_server_request={"headers": {"x-custom": "my-value", "x-empty": ""}}
)
assert result is not None
assert len(result) == 1
assert "x-custom: my-value" in result
assert "x-empty:" not in str(result)
finally:
# Restore original value
if original_extra_headers is not None:
litellm.extra_spend_tag_headers = original_extra_headers
else:
# Remove the attribute if it didn't exist before
if hasattr(litellm, "extra_spend_tag_headers"):
delattr(litellm, "extra_spend_tag_headers")
def test_response_cost_calculator_with_response_cost_in_hidden_params(logging_obj):
from litellm import Router
from litellm.litellm_core_utils.litellm_logging import Logging

View file

@ -242,3 +242,4 @@ def test_cache_read_input_tokens_retained():
assert usage.cache_creation_input_tokens == 4
assert usage.cache_read_input_tokens == 11775
assert usage.prompt_tokens_details.cached_tokens == 11775

View file

@ -8,9 +8,6 @@ sys.path.insert(0, os.path.abspath("../../../../.."))
from unittest.mock import MagicMock, patch
from litellm.llms.anthropic.experimental_pass_through.adapters.streaming_iterator import (
AnthropicStreamWrapper,
)
from litellm.types.utils import Delta, ModelResponse, StreamingChoices
@ -36,6 +33,32 @@ def test_anthropic_experimental_pass_through_messages_handler():
mock_completion.call_args.kwargs["api_key"] == "test-api-key"
def test_anthropic_experimental_pass_through_messages_handler_dynamic_api_key_and_api_base_and_custom_values():
"""
Test that api key is passed to litellm.completion
"""
from litellm.llms.anthropic.experimental_pass_through.messages.handler import (
anthropic_messages_handler,
)
with patch("litellm.completion", return_value="test-response") as mock_completion:
try:
anthropic_messages_handler(
max_tokens=100,
messages=[{"role": "user", "content": "Hello, how are you?"}],
model="azure/o1",
api_key="test-api-key",
api_base="test-api-base",
custom_key="custom_value",
)
except Exception as e:
print(f"Error: {e}")
mock_completion.assert_called_once()
mock_completion.call_args.kwargs["api_key"] == "test-api-key"
mock_completion.call_args.kwargs["api_base"] == "test-api-base"
mock_completion.call_args.kwargs["custom_key"] == "custom_value"
def test_anthropic_experimental_pass_through_messages_handler_custom_llm_provider():
"""
Test that litellm.completion is called when a custom LLM provider is given

View file

@ -56,7 +56,9 @@ class MockCompletionStream:
def test_anthropic_sse_wrapper_format():
"""Test that the SSE wrapper produces proper event and data formatting"""
wrapper = AnthropicStreamWrapper(completion_stream=MockCompletionStream())
wrapper = AnthropicStreamWrapper(
completion_stream=MockCompletionStream(), model="claude-3"
)
# Get the first chunk from the SSE wrapper
first_chunk = next(wrapper.anthropic_sse_wrapper())
@ -77,7 +79,9 @@ def test_anthropic_sse_wrapper_format():
def test_anthropic_sse_wrapper_event_types():
"""Test that different chunk types produce correct event types"""
wrapper = AnthropicStreamWrapper(completion_stream=MockCompletionStream())
wrapper = AnthropicStreamWrapper(
completion_stream=MockCompletionStream(), model="claude-3"
)
chunks = []
for chunk in wrapper.anthropic_sse_wrapper():
@ -134,7 +138,9 @@ async def test_async_anthropic_sse_wrapper():
self.index += 1
return response
wrapper = AnthropicStreamWrapper(completion_stream=AsyncMockCompletionStream())
wrapper = AnthropicStreamWrapper(
completion_stream=AsyncMockCompletionStream(), model="claude-3"
)
# Get the first chunk from the async SSE wrapper
first_chunk = None

View file

@ -751,11 +751,11 @@ async def test_validate_team_member_add_permissions_admin():
# Create admin user
admin_user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN.value)
# Create mock team
team = MagicMock(spec=LiteLLM_TeamTable)
team.team_id = "test-team-123"
# Should not raise any exception for admin
await _validate_team_member_add_permissions(
user_api_key_dict=admin_user,
@ -776,21 +776,21 @@ async def test_validate_team_member_add_permissions_non_admin():
regular_user = UserAPIKeyAuth(
user_id="regular-user",
user_role=LitellmUserRoles.INTERNAL_USER.value,
team_id="different-team"
team_id="different-team",
)
# Create mock team
team = MagicMock(spec=LiteLLM_TeamTable)
team.team_id = "test-team-123"
team.members_with_roles = []
# Mock the helper functions to return False
with patch(
"litellm.proxy.management_endpoints.team_endpoints._is_user_team_admin",
return_value=False
return_value=False,
), patch(
"litellm.proxy.management_endpoints.team_endpoints._is_available_team",
return_value=False
return_value=False,
):
# Should raise HTTPException for non-admin
with pytest.raises(HTTPException) as exc_info:
@ -798,7 +798,7 @@ async def test_validate_team_member_add_permissions_non_admin():
user_api_key_dict=regular_user,
complete_team_data=team,
)
assert exc_info.value.status_code == 403
assert "not proxy admin OR team admin" in str(exc_info.value.detail)
@ -808,30 +808,30 @@ async def test_process_team_members_single_member():
"""
Test _process_team_members with a single member
"""
from litellm.proxy._types import LiteLLM_TeamMembership, LiteLLM_UserTable
from litellm.proxy.management_endpoints.team_endpoints import _process_team_members
from litellm.proxy._types import LiteLLM_UserTable, LiteLLM_TeamMembership
# Mock dependencies
mock_prisma_client = MagicMock()
mock_team = MagicMock(spec=LiteLLM_TeamTable)
mock_team.metadata = {"team_member_budget_id": "budget-123"}
# Mock user and membership objects
mock_user = MagicMock(spec=LiteLLM_UserTable)
mock_user.user_id = "new-user-123"
mock_membership = MagicMock(spec=LiteLLM_TeamMembership)
# Create request with single member
single_member = Member(user_email="new@example.com", role="user")
request_data = TeamMemberAddRequest(
team_id="test-team-123",
member=single_member,
)
with patch(
"litellm.proxy.management_endpoints.team_endpoints.add_new_member",
new_callable=AsyncMock,
return_value=(mock_user, mock_membership)
return_value=(mock_user, mock_membership),
) as mock_add_member:
users, memberships = await _process_team_members(
data=request_data,
@ -840,13 +840,13 @@ async def test_process_team_members_single_member():
user_api_key_dict=UserAPIKeyAuth(),
litellm_proxy_admin_name="admin",
)
# Verify results
assert len(users) == 1
assert len(memberships) == 1
assert users[0] == mock_user
assert memberships[0] == mock_membership
# Verify add_new_member was called correctly
mock_add_member.assert_called_once_with(
new_member=single_member,
@ -864,14 +864,14 @@ async def test_process_team_members_multiple_members():
"""
Test _process_team_members with multiple members
"""
from litellm.proxy._types import LiteLLM_TeamMembership, LiteLLM_UserTable
from litellm.proxy.management_endpoints.team_endpoints import _process_team_members
from litellm.proxy._types import LiteLLM_UserTable, LiteLLM_TeamMembership
# Mock dependencies
mock_prisma_client = MagicMock()
mock_team = MagicMock(spec=LiteLLM_TeamTable)
mock_team.metadata = None
# Create multiple members as dictionaries (they will be converted to Member objects)
members = [
{"user_email": "user1@example.com", "role": "user"},
@ -882,15 +882,18 @@ async def test_process_team_members_multiple_members():
member=members,
max_budget_in_team=100.0,
)
# Mock different users and memberships for each call
mock_users = [MagicMock(spec=LiteLLM_UserTable) for _ in range(2)]
mock_memberships = [MagicMock(spec=LiteLLM_TeamMembership) for _ in range(2)]
with patch(
"litellm.proxy.management_endpoints.team_endpoints.add_new_member",
new_callable=AsyncMock,
side_effect=[(mock_users[0], mock_memberships[0]), (mock_users[1], mock_memberships[1])]
side_effect=[
(mock_users[0], mock_memberships[0]),
(mock_users[1], mock_memberships[1]),
],
) as mock_add_member:
users, memberships = await _process_team_members(
data=request_data,
@ -899,13 +902,13 @@ async def test_process_team_members_multiple_members():
user_api_key_dict=UserAPIKeyAuth(),
litellm_proxy_admin_name="admin",
)
# Verify results
assert len(users) == 2
assert len(memberships) == 2
assert users == mock_users
assert memberships == mock_memberships
# Verify add_new_member was called for each member
assert mock_add_member.call_count == 2
@ -915,33 +918,33 @@ async def test_update_team_members_list_single_member():
"""
Test _update_team_members_list with a single member
"""
from litellm.proxy.management_endpoints.team_endpoints import _update_team_members_list
from litellm.proxy._types import LiteLLM_UserTable
from litellm.proxy.management_endpoints.team_endpoints import (
_update_team_members_list,
)
# Create mock team with existing members
mock_team = MagicMock(spec=LiteLLM_TeamTable)
mock_team.members_with_roles = [
Member(user_id="existing-user", role="admin")
]
mock_team.members_with_roles = [Member(user_id="existing-user", role="admin")]
# Create new member without user_id
new_member = Member(user_email="new@example.com", role="user")
request_data = TeamMemberAddRequest(
team_id="test-team-123",
member=new_member,
)
# Create mock user with matching email
mock_user = MagicMock(spec=LiteLLM_UserTable)
mock_user.user_id = "new-user-123"
mock_user.user_email = "new@example.com"
await _update_team_members_list(
data=request_data,
complete_team_data=mock_team,
updated_users=[mock_user],
)
# Verify member was added
assert len(mock_team.members_with_roles) == 2
added_member = mock_team.members_with_roles[1]
@ -955,32 +958,53 @@ async def test_update_team_members_list_duplicate_prevention():
"""
Test _update_team_members_list prevents duplicate members
"""
from litellm.proxy.management_endpoints.team_endpoints import _update_team_members_list
from litellm.proxy._types import LiteLLM_UserTable
from litellm.proxy.management_endpoints.team_endpoints import (
_update_team_members_list,
)
# Create mock team with existing members
mock_team = MagicMock(spec=LiteLLM_TeamTable)
mock_team.members_with_roles = [
Member(user_id="existing-user", user_email="existing@example.com", role="admin")
]
# Try to add the same member again
duplicate_member = Member(user_id="existing-user", role="user")
request_data = TeamMemberAddRequest(
team_id="test-team-123",
member=duplicate_member,
)
# Create mock user
mock_user = MagicMock(spec=LiteLLM_UserTable)
mock_user.user_id = "existing-user"
mock_user.user_email = "existing@example.com"
await _update_team_members_list(
data=request_data,
complete_team_data=mock_team,
updated_users=[mock_user],
)
# Verify member was NOT added (still only 1 member)
assert len(mock_team.members_with_roles) == 1
def test_add_new_models_to_team_with_existing_models():
"""
Test add_new_models_to_team function with existing models
"""
from litellm.proxy._types import SpecialModelNames
from litellm.proxy.management_endpoints.team_endpoints import add_new_models_to_team
team_obj = MagicMock(spec=LiteLLM_TeamTable)
team_obj.models = ["model1", "model2"]
new_models = ["model3", "model4"]
updated_models = add_new_models_to_team(
team_obj=team_obj,
new_models=new_models,
)
assert updated_models.sort() == ["model1", "model2", "model3", "model4"].sort()

View file

@ -320,7 +320,7 @@ async def test_get_all_team_models():
# Mock router
mock_router = MagicMock()
def mock_get_model_list(model_name):
def mock_get_model_list(model_name, team_id=None):
if model_name == "gpt-4":
return mock_models_gpt4
elif model_name == "gpt-3.5-turbo":
@ -355,10 +355,10 @@ async def test_get_all_team_models():
# Verify router.get_model_list was called for each model
expected_calls = [
mock.call(model_name="gpt-4"),
mock.call(model_name="gpt-3.5-turbo"),
mock.call(model_name="claude-3"),
mock.call(model_name="gpt-4"), # Called again for team2
mock.call(model_name="gpt-4", team_id="team1"),
mock.call(model_name="gpt-3.5-turbo", team_id="team1"),
mock.call(model_name="claude-3", team_id="team2"),
mock.call(model_name="gpt-4", team_id="team2"),
]
mock_router.get_model_list.assert_has_calls(expected_calls, any_order=True)
@ -386,8 +386,8 @@ async def test_get_all_team_models():
# Verify router.get_model_list was called only for team1 models
expected_calls = [
mock.call(model_name="gpt-4"),
mock.call(model_name="gpt-3.5-turbo"),
mock.call(model_name="gpt-4", team_id="team1"),
mock.call(model_name="gpt-3.5-turbo", team_id="team1"),
]
mock_router.get_model_list.assert_has_calls(expected_calls, any_order=True)
@ -413,7 +413,7 @@ async def test_get_all_team_models():
mock_router.reset_mock()
mock_litellm_teamtable.find_many.return_value = [mock_team1]
def mock_get_model_list_with_none(model_name):
def mock_get_model_list_with_none(model_name, team_id=None):
if model_name == "gpt-4":
return mock_models_gpt4
# Return None for gpt-3.5-turbo to test None handling

View file

@ -481,3 +481,124 @@ async def test_router_filter_team_based_models():
)
assert result is not None
def test_router_should_include_deployment():
"""
Test the should_include_deployment method with various scenarios
The method logic:
1. Returns True if: team_id matches AND model_name matches team_public_model_name
2. Returns True if: model_name matches AND deployment has no team_id
3. Otherwise returns False
"""
router = litellm.Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {"model": "gpt-3.5-turbo"},
"model_info": {
"team_id": "test-team",
},
},
],
)
# Test deployment structures
deployment_with_team_and_public_name = {
"model_name": "gpt-3.5-turbo",
"model_info": {
"team_id": "test-team",
"team_public_model_name": "team-gpt-model",
},
}
deployment_with_team_no_public_name = {
"model_name": "gpt-3.5-turbo",
"model_info": {
"team_id": "test-team",
},
}
deployment_without_team = {
"model_name": "gpt-4",
"model_info": {},
}
deployment_different_team = {
"model_name": "claude-3",
"model_info": {
"team_id": "other-team",
"team_public_model_name": "team-claude-model",
},
}
# Test Case 1: Team-specific deployment - team_id and team_public_model_name match
result = router.should_include_deployment(
model_name="team-gpt-model",
model=deployment_with_team_and_public_name,
team_id="test-team",
)
assert (
result is True
), "Should return True when team_id and team_public_model_name match"
# Test Case 2: Team-specific deployment - team_id matches but model_name doesn't match team_public_model_name
result = router.should_include_deployment(
model_name="different-model",
model=deployment_with_team_and_public_name,
team_id="test-team",
)
assert (
result is False
), "Should return False when team_id matches but model_name doesn't match team_public_model_name"
# Test Case 3: Team-specific deployment - team_id doesn't match
result = router.should_include_deployment(
model_name="team-gpt-model",
model=deployment_with_team_and_public_name,
team_id="different-team",
)
assert result is False, "Should return False when team_id doesn't match"
# Test Case 4: Team-specific deployment with no team_public_model_name - should fail
result = router.should_include_deployment(
model_name="gpt-3.5-turbo",
model=deployment_with_team_no_public_name,
team_id="test-team",
)
assert (
result is True
), "Should return True when team deployment has no team_public_model_name to match"
# Test Case 5: Non-team deployment - model_name matches and no team_id
result = router.should_include_deployment(
model_name="gpt-4", model=deployment_without_team, team_id=None
)
assert (
result is True
), "Should return True when model_name matches and deployment has no team_id"
# Test Case 6: Non-team deployment - model_name matches but team_id provided (should still work)
result = router.should_include_deployment(
model_name="gpt-4", model=deployment_without_team, team_id="any-team"
)
assert (
result is True
), "Should return True when model_name matches non-team deployment, regardless of team_id param"
# Test Case 7: Non-team deployment - model_name doesn't match
result = router.should_include_deployment(
model_name="different-model", model=deployment_without_team, team_id=None
)
assert result is False, "Should return False when model_name doesn't match"
# Test Case 8: Team deployment accessed without matching team_id
result = router.should_include_deployment(
model_name="gpt-3.5-turbo",
model=deployment_with_team_and_public_name,
team_id=None,
)
assert (
result is True
), "Should return True when matching model with exact model_name"

View file

@ -19,7 +19,7 @@ import {
} from "./email_events/types";
const isLocal = process.env.NODE_ENV === "development";
export const defaultProxyBaseUrl = isLocal ? "http://localhost:43845" : null;
export const defaultProxyBaseUrl = isLocal ? "http://localhost:4000" : null;
const defaultServerRootPath = "/";
export let serverRootPath = defaultServerRootPath;
export let proxyBaseUrl = defaultProxyBaseUrl;

View file

@ -67,7 +67,11 @@ const TeamMembersComponent: React.FC<TeamMembersComponentProps> = ({
if (!userId) return null;
const membership = teamData.team_memberships.find(tm => tm.user_id === userId);
console.log(`membership=${membership}`);
return formatNumber(membership?.litellm_budget_table?.max_budget || null);
const maxBudget = membership?.litellm_budget_table?.max_budget;
if (maxBudget === null || maxBudget === undefined) {
return null;
}
return formatNumber(maxBudget);
};
return (