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>
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
:::
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -124,7 +124,6 @@ Expected response on failure:
|
|||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Successful Call" value="allowed">
|
||||
|
||||
```shell
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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')} />
|
||||
BIN
docs/my-website/img/claude_cli_tag_usage.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
docs/my-website/img/custom_tag_headers.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
docs/my-website/img/release_notes/batch_api_cost_tracking.jpg
Normal file
|
After Width: | Height: | Size: 491 KiB |
BIN
docs/my-website/img/release_notes/gemini_cli.png
Normal file
|
After Width: | Height: | Size: 571 KiB |
277
docs/my-website/release_notes/v1.73.6-stable/index.md
Normal 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)**
|
||||
BIN
enterprise/dist/litellm_enterprise-0.1.10-py3-none-any.whl
vendored
Normal file
BIN
enterprise/dist/litellm_enterprise-0.1.10.tar.gz
vendored
Normal file
4
enterprise/poetry.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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==",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))}();
|
||||
|
|
@ -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}
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
||||
|
|
|
|||
38
litellm/types/secret_managers/main.py
Normal 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
|
||||
"""
|
||||
|
|
@ -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
|
|
@ -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"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -57,4 +57,4 @@ websockets==13.1.0 # for realtime API
|
|||
########################
|
||||
# LITELLM ENTERPRISE DEPENDENCIES
|
||||
########################
|
||||
litellm-enterprise==0.1.9
|
||||
litellm-enterprise==0.1.10
|
||||
|
|
|
|||
98
tests/code_coverage_tests/test_proxy_types_import.py
Normal 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()
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||