This commit is contained in:
Jean Carlo de Souza 2025-04-24 01:02:01 -07:00 committed by GitHub
commit 29eea8fe9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 432 additions and 4 deletions

View file

@ -17,15 +17,20 @@ LiteLLM currently covers:
## **Supported Providers**:
- [OpenAI](#quick-start)
- [Azure OpenAI](#azure-openai)
- [OpenAI-Compatible APIs](#openai-compatible-apis)
- [Assistants API](#assistants-api)
- [**Supported Providers**:](#supported-providers)
- [Quick Start](#quick-start)
- [SDK + PROXY](#sdk--proxy)
- [Streaming](#streaming)
- [👉 Proxy API Reference](#-proxy-api-reference)
- [Azure OpenAI](#azure-openai)
- [OpenAI-Compatible APIs](#openai-compatible-apis)
## Quick Start
Call an existing Assistant.
- Get the Assistant
- Get the Assistant
- Create a Thread when a user starts a conversation.
@ -33,6 +38,8 @@ Call an existing Assistant.
- Run the Assistant on the Thread to generate a response by calling the model and the tools.
- Modify the Assistant's details if necessary (name, instructions, tools, etc.)
### SDK + PROXY
<Tabs>
<TabItem value="sdk" label="SDK">
@ -80,6 +87,36 @@ assistants = get_assistants(custom_llm_provider="openai")
# assistants = await aget_assistants(custom_llm_provider="openai")
```
**Modify the Assistant**
```python
from litellm import modify_assistants, amodify_assistants
# setup env
os.environ["OPENAI_API_KEY"] = "sk-.."
# Modify Assistant (sync)
modified_assistant = modify_assistants(
custom_llm_provider="openai",
assistant_id="asst_S4IW1JplxrRtsb5sQOXmztf4",
model="gpt-4o",
name="Updated Python Assistant",
instructions="You are an advanced Python tutor.",
tools=[{"type": "code_interpreter"}],
)
### ASYNC USAGE ###
# modified_assistant = await amodify_assistants(
# custom_llm_provider="openai",
# assistant_id="asst_S4IW1JplxrRtsb5sQOXmztf4",
# model="gpt-4o",
# name="Updated Python Assistant",
# instructions="You are an advanced Python tutor.",
# tools=[{"type": "code_interpreter"}],
# )
```
**Create a Thread**
```python
@ -246,6 +283,19 @@ curl http://0.0.0.0:4000/v1/threads/thread_abc123/runs \
}'
```
**Modify an Assistant**
```bash
curl -X PATCH "http://0.0.0.0:4000/v1/assistants/{assistant_id}" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-1234" \
-d '{
"name": "Updated Math Tutor",
"instructions": "You are a Python coding expert who can solve complex math problems.",
"tools": [{"type": "code_interpreter"}],
"model": "gpt-4o"
}'
```
</TabItem>
</Tabs>

View file

@ -581,6 +581,155 @@ def delete_assistant(
)
return response
async def amodify_assistants(
custom_llm_provider: Literal["openai", "azure"],
assistant_id: str,
**kwargs,
) -> Assistant:
loop = asyncio.get_event_loop()
kwargs["async_modify_assistants"] = True
kwargs = {k: v for k, v in kwargs.items() if v is not None}
func = partial(modify_assistants, custom_llm_provider, assistant_id, **kwargs)
ctx = contextvars.copy_context()
func_with_context = partial(ctx.run, func)
_, custom_llm_provider, _, _ = get_llm_provider( # type: ignore
model=kwargs.get("model", ""), custom_llm_provider=custom_llm_provider
) # type: ignore
try:
init_response = await loop.run_in_executor(None, func_with_context)
if asyncio.iscoroutine(init_response):
response = await init_response
else:
response = init_response
return response # type: ignore
except Exception as e:
raise exception_type(
model=kwargs.get("model", ""),
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs={},
extra_kwargs=kwargs,
)
def modify_assistants(
custom_llm_provider: Literal["openai", "azure"],
assistant_id: str,
model: Optional[str] = None,
name: Optional[str] = None,
description: Optional[str] = None,
instructions: Optional[str] = None,
tools: Optional[List[Dict[str, Any]]] = None,
tool_resources: Optional[Dict[str, Any]] = None,
metadata: Optional[Dict[str, str]] = None,
temperature: Optional[float] = None,
top_p: Optional[float] = None,
response_format: Optional[Union[str, Dict[str, str]]] = None,
client: Optional[Any] = None,
api_key: Optional[str] = None,
api_base: Optional[str] = None,
api_version: Optional[str] = None,
**kwargs,
) -> Union[Assistant, Coroutine[Any, Any, Assistant]]:
async_modify_assistants: Optional[bool] = kwargs.pop("async_modify_assistants", None)
if async_modify_assistants is not None and not isinstance(async_modify_assistants, bool):
raise ValueError(
"Invalid value passed in for async_modify_assistants. Only bool or None allowed."
)
optional_params = GenericLiteLLMParams(
api_key=api_key, api_base=api_base, api_version=api_version, **kwargs
)
timeout = optional_params.timeout or kwargs.get("request_timeout", 600) or 600
if (
timeout is not None
and isinstance(timeout, httpx.Timeout)
and supports_httpx_timeout(custom_llm_provider) is False
):
read_timeout = timeout.read or 600
timeout = read_timeout
elif timeout is not None and not isinstance(timeout, httpx.Timeout):
timeout = float(timeout) # type: ignore
elif timeout is None:
timeout = 600.0
modify_assistant_data = {
"model": model,
"name": name,
"description": description,
"instructions": instructions,
"tools": tools,
"tool_resources": tool_resources,
"metadata": metadata,
"temperature": temperature,
"top_p": top_p,
"response_format": response_format,
}
modify_assistant_data = {k: v for k, v in modify_assistant_data.items() if v is not None}
response: Optional[Union[Coroutine[Any, Any, Assistant], Assistant]] = None
if custom_llm_provider == "openai":
api_base = (
optional_params.api_base
or litellm.api_base
or os.getenv("OPENAI_API_BASE")
or "https://api.openai.com/v1"
)
organization = (
optional_params.organization
or litellm.organization
or os.getenv("OPENAI_ORGANIZATION", None)
or None
)
api_key = (
optional_params.api_key
or litellm.api_key
or litellm.openai_key
or os.getenv("OPENAI_API_KEY")
)
response = openai_assistants_api.modify_assistants(
assistant_id=assistant_id,
api_base=api_base,
api_key=api_key,
timeout=timeout,
max_retries=optional_params.max_retries,
organization=organization,
modify_assistant_data=modify_assistant_data,
client=client,
async_modify_assistants=async_modify_assistants, # type: ignore
)
else:
raise litellm.exceptions.BadRequestError(
message="LiteLLM doesn't support {} for 'modify_assistants'. Only 'openai' is supported.".format(
custom_llm_provider
),
model="n/a",
llm_provider=custom_llm_provider,
response=httpx.Response(
status_code=400,
content="Unsupported provider",
request=httpx.Request(method="update_thread", url="https://github.com/BerriAI/litellm"), # type: ignore
),
)
if response is None:
raise litellm.exceptions.InternalServerError(
message="No response returned from 'modify_assistants'",
model=model or "n/a",
llm_provider=custom_llm_provider,
)
return response
### THREADS ###

View file

@ -2222,6 +2222,69 @@ class OpenAIAssistantsAPI(BaseLLM):
response = openai_client.beta.assistants.delete(assistant_id=assistant_id)
return response
# Modify Assistant
async def async_modify_assistants(
self,
assistant_id: str,
api_key: Optional[str],
api_base: Optional[str],
timeout: Union[float, httpx.Timeout],
max_retries: Optional[int],
organization: Optional[str],
client: Optional[AsyncOpenAI],
modify_assistant_data: dict,
) -> Assistant:
openai_client = self.async_get_openai_client(
api_key=api_key,
api_base=api_base,
timeout=timeout,
max_retries=max_retries,
organization=organization,
client=client,
)
response = await openai_client.beta.assistants.update(assistant_id, **modify_assistant_data)
return response
def modify_assistants(
self,
assistant_id: str,
api_key: Optional[str],
api_base: Optional[str],
timeout: Union[float, httpx.Timeout],
max_retries: Optional[int],
organization: Optional[str],
modify_assistant_data: dict,
client=None,
async_modify_assistants=None,
):
if async_modify_assistants is not None and async_modify_assistants is True:
return self.async_modify_assistants(
assistant_id=assistant_id,
api_key=api_key,
api_base=api_base,
timeout=timeout,
max_retries=max_retries,
organization=organization,
client=client,
modify_assistant_data=modify_assistant_data,
)
openai_client = self.get_openai_client(
api_key=api_key,
api_base=api_base,
timeout=timeout,
max_retries=max_retries,
organization=organization,
client=client,
)
response = openai_client.beta.assistants.update(assistant_id, **modify_assistant_data)
return response
### MESSAGES ###
async def a_add_message(

View file

@ -4630,6 +4630,100 @@ async def delete_assistant(
)
@router.patch(
"/v1/assistants/{assistant_id}",
dependencies=[Depends(user_api_key_auth)],
tags=["assistants"],
)
@router.patch(
"/assistants/{assistant_id}",
dependencies=[Depends(user_api_key_auth)],
tags=["assistants"],
)
async def modify_assistant(
assistant_id: str,
request: Request,
fastapi_response: Response,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Modify assistant
API Reference docs - https://platform.openai.com/docs/api-reference/assistants/modifyAssistant
"""
global proxy_logging_obj
data = {}
try:
body = await request.body()
data = orjson.loads(body)
data = await add_litellm_data_to_request(
data=data,
request=request,
general_settings=general_settings,
user_api_key_dict=user_api_key_dict,
version=version,
proxy_config=proxy_config,
)
data["assistant_id"] = assistant_id
if llm_router is None:
raise HTTPException(
status_code=500, detail={"error": CommonProxyErrors.no_llm_router.value}
)
response = await llm_router.amodify_assistants(**data)
asyncio.create_task(
proxy_logging_obj.update_request_status(
litellm_call_id=data.get("litellm_call_id", ""), status="success"
)
)
hidden_params = getattr(response, "_hidden_params", {}) or {}
model_id = hidden_params.get("model_id", "") or ""
cache_key = hidden_params.get("cache_key", "") or ""
api_base = hidden_params.get("api_base", "") or ""
fastapi_response.headers.update(
get_custom_headers(
user_api_key_dict=user_api_key_dict,
model_id=model_id,
cache_key=cache_key,
api_base=api_base,
version=version,
model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
request_data=data,
)
)
return response
except Exception as e:
await proxy_logging_obj.post_call_failure_hook(
user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
)
verbose_proxy_logger.error(
f"litellm.proxy.proxy_server.modify_assistant(): Exception occurred - {str(e)}"
)
verbose_proxy_logger.debug(traceback.format_exc())
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "message", str(e.detail)),
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
)
else:
error_msg = f"{str(e)}"
raise ProxyException(
message=error_msg,
type=getattr(e, "type", "None"),
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", 500),
)
@router.post(
"/v1/threads",
dependencies=[Depends(user_api_key_auth)],

View file

@ -721,6 +721,7 @@ class Router:
self.a_add_message = self.factory_function(litellm.a_add_message)
self.aget_messages = self.factory_function(litellm.aget_messages)
self.arun_thread = self.factory_function(litellm.arun_thread)
self.amodify_assistants = self.factory_function(litellm.amodify_assistants)
def initialize_router_endpoints(self):
self.amoderation = self.factory_function(

View file

@ -329,3 +329,73 @@ async def test_aarun_thread_litellm(sync_mode, provider, is_streaming):
)
except openai.APIError as e:
pass
@pytest.mark.parametrize("provider", ["openai"])
@pytest.mark.parametrize("sync_mode", [True, False])
@pytest.mark.asyncio
async def test_modify_assistant(provider, sync_mode):
"""
Test for modifying an assistant's configuration.
"""
model = "gpt-4o"
assistant_name = "Test Assistant"
initial_instructions = "You are a coding assistant."
modified_instructions = "You are an advanced coding assistant specialized in Python."
# 1. Assistant creation
if sync_mode:
assistant = litellm.create_assistants(
custom_llm_provider=provider,
model=model,
instructions=initial_instructions,
name=assistant_name,
tools=[{"type": "code_interpreter"}],
)
else:
assistant = await litellm.acreate_assistants(
custom_llm_provider=provider,
model=model,
instructions=initial_instructions,
name=assistant_name,
tools=[{"type": "code_interpreter"}],
)
assert isinstance(assistant, Assistant)
assert assistant.instructions == initial_instructions
assert assistant.id is not None
# 2. Assistant modification
if sync_mode:
modified_assistant = litellm.modify_assistants(
custom_llm_provider=provider,
model=model,
assistant_id=assistant.id,
instructions=modified_instructions,
name="Modified Test Assistant",
)
else:
modified_assistant = await litellm.amodify_assistants(
custom_llm_provider=provider,
model=model,
assistant_id=assistant.id,
instructions=modified_instructions,
name="Modified Test Assistant",
)
# 3. Assert modifications
assert modified_assistant.instructions == modified_instructions
assert modified_assistant.name == "Modified Test Assistant"
assert modified_assistant.id == assistant.id
# 4. Clean created assistant for test
if sync_mode:
response = litellm.delete_assistant(
custom_llm_provider=provider, assistant_id=assistant.id
)
else:
response = await litellm.adelete_assistant(
custom_llm_provider=provider, assistant_id=assistant.id
)
assert response.id == assistant.id

View file

@ -938,6 +938,7 @@ def test_initialize_assistants_endpoint(model_list):
assert router.acreate_assistants is not None
assert router.adelete_assistant is not None
assert router.aget_assistants is not None
assert router.amodify_assistants is not None
assert router.acreate_thread is not None
assert router.aget_thread is not None
assert router.arun_thread is not None