From e40ad5203eb3a6a07a7d171f60f642f97ac75666 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 16 Dec 2025 09:28:04 +0530 Subject: [PATCH 1/5] Add container field as provider specific field --- litellm/llms/anthropic/chat/transformation.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 628121ab11..72c95eef12 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1132,22 +1132,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if content["type"] == "text": text_content += content["text"] ## TOOL CALLING - elif content["type"] == "tool_use": + elif content["type"] == "tool_use" or content["type"] == "server_tool_use": tool_call = AnthropicConfig.convert_tool_use_to_openai_format( anthropic_tool_content=content, index=idx, ) tool_calls.append(tool_call) - ## SERVER TOOL USE (for tool search) - elif content["type"] == "server_tool_use": - # Server tool use blocks are for tool search - treat as tool calls - # Note: using .get("input", {}) for server_tool_use as input may not be present - content_with_input = {**content, "input": content.get("input", {})} - tool_call = AnthropicConfig.convert_tool_use_to_openai_format( - anthropic_tool_content=content_with_input, - index=idx, - ) - tool_calls.append(tool_call) ## TOOL SEARCH TOOL RESULT (skip - this is metadata about tool discovery) elif content["type"] == "tool_search_tool_result": # This block contains tool_references that were discovered @@ -1343,6 +1333,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): "context_management" ) + container: Optional[Dict] = completion_response.get("container") + provider_specific_fields: Dict[str, Any] = { "citations": citations, "thinking_blocks": thinking_blocks, @@ -1351,7 +1343,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): provider_specific_fields["context_management"] = context_management if web_search_results is not None: provider_specific_fields["web_search_results"] = web_search_results - + if container is not None: + provider_specific_fields["container"] = container + _message = litellm.Message( tool_calls=tool_calls, content=text_content or None, From 9802a6a19ea44df04b02cdb718807d49ea95d619 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 16 Dec 2025 10:02:07 +0530 Subject: [PATCH 2/5] Add container field in streaming response --- litellm/llms/anthropic/chat/handler.py | 29 +++++++++----------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 90c8c30eed..1f86e107d0 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -690,14 +690,14 @@ class ModelResponseIterator: self.current_content_block_type = content_block_start["content_block"]["type"] if content_block_start["content_block"]["type"] == "text": text = content_block_start["content_block"]["text"] - elif content_block_start["content_block"]["type"] == "tool_use": + elif content_block_start["content_block"]["type"] == "tool_use" or content_block_start["content_block"]["type"] == "server_tool_use": self.tool_index += 1 tool_use = ChatCompletionToolCallChunk( id=content_block_start["content_block"]["id"], type="function", function=ChatCompletionToolCallFunctionChunk( name=content_block_start["content_block"]["name"], - arguments="", + arguments=content_block_start["content_block"]["input"], ), index=self.tool_index, ) @@ -706,18 +706,6 @@ class ModelResponseIterator: caller_data = content_block_start["content_block"]["caller"] if caller_data: tool_use["caller"] = cast(Dict[str, Any], caller_data) # type: ignore[typeddict-item] - elif content_block_start["content_block"]["type"] == "server_tool_use": - # Handle server tool use (for tool search) - self.tool_index += 1 - tool_use = ChatCompletionToolCallChunk( - id=content_block_start["content_block"]["id"], - type="function", - function=ChatCompletionToolCallFunctionChunk( - name=content_block_start["content_block"]["name"], - arguments="", - ), - index=self.tool_index, - ) elif ( content_block_start["content_block"]["type"] == "redacted_thinking" ): @@ -765,7 +753,9 @@ class ModelResponseIterator: # These are automatically handled by Anthropic API, we just pass them through pass elif type_chunk == "message_delta": - finish_reason, usage = self._handle_message_delta(chunk) + finish_reason, usage, container = self._handle_message_delta(chunk) + if container: + provider_specific_fields["container"] = container elif type_chunk == "message_start": """ Anthropic @@ -881,15 +871,15 @@ class ModelResponseIterator: return text, tool_use - def _handle_message_delta(self, chunk: dict) -> Tuple[str, Optional[Usage]]: + def _handle_message_delta(self, chunk: dict) -> Tuple[str, Optional[Usage], Optional[Dict[str, Any]]]: """ - Handle message_delta event for finish_reason and usage. + Handle message_delta event for finish_reason, usage, and container. Args: chunk: The message_delta chunk Returns: - Tuple of (finish_reason, usage) + Tuple of (finish_reason, usage, container) """ message_delta = MessageBlockDelta(**chunk) # type: ignore finish_reason = map_finish_reason( @@ -900,7 +890,8 @@ class ModelResponseIterator: if self.converted_response_format_tool: finish_reason = "stop" usage = self._handle_usage(anthropic_usage_chunk=message_delta["usage"]) - return finish_reason, usage + container = message_delta["delta"].get("container") + return finish_reason, usage, container def _handle_accumulated_json_chunk( self, data_str: str From bcfc77f68307ba249b61d95e3a5807439431ad11 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 16 Dec 2025 10:12:36 +0530 Subject: [PATCH 3/5] Add beta headers for claude skills --- litellm/llms/anthropic/common_utils.py | 45 ++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 7ca3c55554..098694f15a 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -186,6 +186,37 @@ class AnthropicModelInfo(BaseLLMModelInfo): return False + def is_code_execution_tool_used(self, tools: Optional[List]) -> bool: + """ + Check if code execution tool is being used. + + Returns True if any tool has type "code_execution_20250825". + """ + if not tools: + return False + + for tool in tools: + tool_type = tool.get("type", "") + if tool_type == "code_execution_20250825": + return True + return False + + def is_container_with_skills_used(self, optional_params: Optional[dict]) -> bool: + """ + Check if container with skills is being used. + + Returns True if optional_params contains container with skills. + """ + if not optional_params: + return False + + container = optional_params.get("container") + if container and isinstance(container, dict): + skills = container.get("skills") + if skills and isinstance(skills, list) and len(skills) > 0: + return True + return False + def _get_user_anthropic_beta_headers( self, anthropic_beta_header: Optional[str] ) -> Optional[List[str]]: @@ -270,6 +301,8 @@ class AnthropicModelInfo(BaseLLMModelInfo): effort_used: bool = False, is_vertex_request: bool = False, user_anthropic_beta_headers: Optional[List[str]] = None, + code_execution_tool_used: bool = False, + container_with_skills_used: bool = False, ) -> dict: betas = set() if prompt_caching_set: @@ -293,6 +326,14 @@ class AnthropicModelInfo(BaseLLMModelInfo): if effort_used: from litellm.types.llms.anthropic import ANTHROPIC_EFFORT_BETA_HEADER betas.add(ANTHROPIC_EFFORT_BETA_HEADER) + + # Code execution tool uses a separate beta header + if code_execution_tool_used: + betas.add("code-execution-2025-08-25") + + # Container with skills uses a separate beta header + if container_with_skills_used: + betas.add("skills-2025-10-02") headers = { "anthropic-version": anthropic_version or "2023-06-01", @@ -345,6 +386,8 @@ class AnthropicModelInfo(BaseLLMModelInfo): programmatic_tool_calling_used = self.is_programmatic_tool_calling_used(tools=tools) input_examples_used = self.is_input_examples_used(tools=tools) effort_used = self.is_effort_used(optional_params=optional_params, model=model) + code_execution_tool_used = self.is_code_execution_tool_used(tools=tools) + container_with_skills_used = self.is_container_with_skills_used(optional_params=optional_params) user_anthropic_beta_headers = self._get_user_anthropic_beta_headers( anthropic_beta_header=headers.get("anthropic-beta") ) @@ -362,6 +405,8 @@ class AnthropicModelInfo(BaseLLMModelInfo): programmatic_tool_calling_used=programmatic_tool_calling_used, input_examples_used=input_examples_used, effort_used=effort_used, + code_execution_tool_used=code_execution_tool_used, + container_with_skills_used=container_with_skills_used, ) headers = {**headers, **anthropic_headers} From 1222d9e37622563f7ebea969ff595788bc712e49 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 16 Dec 2025 10:20:21 +0530 Subject: [PATCH 4/5] Add doc and tests for agent skils --- docs/my-website/docs/providers/anthropic.md | 84 ++++++++ ...odel_prices_and_context_window_backup.json | 2 +- .../chat/test_anthropic_chat_handler.py | 194 +++++++++++++++++- 3 files changed, 278 insertions(+), 2 deletions(-) diff --git a/docs/my-website/docs/providers/anthropic.md b/docs/my-website/docs/providers/anthropic.md index f78af51bd9..bcfb698a0f 100644 --- a/docs/my-website/docs/providers/anthropic.md +++ b/docs/my-website/docs/providers/anthropic.md @@ -1936,3 +1936,87 @@ curl http://0.0.0.0:4000/v1/chat/completions \ + +## Usage - Agent Skills + +LiteLLM supports using Agent Skills with the API + + + + +```python +response = completion( + model="claude-sonnet-4-5-20250929", + messages=messages, + tools= [ + { + "type": "code_execution_20250825", + "name": "code_execution" + } + ], + container= { + "skills": [ + { + "type": "anthropic", + "skill_id": "pptx", + "version": "latest" + } + ] + } +) +``` + + + +1. Setup config.yaml + +```yaml +model_list: + - model_name: claude-sonnet-4-5-20250929 + litellm_params: + model: anthropic/claude-sonnet-4-5-20250929 + api_key: os.environ/ANTHROPIC_API_KEY +``` + +2. Start Proxy + +``` +litellm --config /path/to/config.yaml +``` + +3. Test it! + +```bash +curl --location 'http://localhost:4000/chat/completions' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer ' \ +--data '{ + "model": "claude-sonnet-4-5-20250929", + "messages": [ + { + "role": "user", + "content": "Hi" + } + ], + "tools": [ + { + "type": "code_execution_20250825", + "name": "code_execution" + } + ], + "container": { + "skills": [ + { + "type": "anthropic", + "skill_id": "pptx", + "version": "latest" + } + ] + } +}' +``` + + + + +The container and its "id" will be present in "provider_specific_fields" in streaming/non-streaming response \ No newline at end of file diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 01d7f076ed..2a7f8aa3dd 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -30628,7 +30628,7 @@ "litellm_provider": "fireworks_ai", "mode": "embedding" }, - "fireworks_ai/accounts/fireworks/models/qwen3-embedding-8b": { + "fireworks_ai/accounts/fireworks/models/": { "max_tokens": 40960, "max_input_tokens": 40960, "max_output_tokens": 40960, diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py index e96d6cc61a..41febd4920 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py @@ -1,11 +1,11 @@ from unittest.mock import MagicMock +from litellm.constants import RESPONSE_FORMAT_TOOL_NAME from litellm.llms.anthropic.chat.handler import ModelResponseIterator from litellm.types.llms.openai import ( ChatCompletionToolCallChunk, ChatCompletionToolCallFunctionChunk, ) -from litellm.constants import RESPONSE_FORMAT_TOOL_NAME def test_redacted_thinking_content_block_delta(): @@ -779,3 +779,195 @@ def test_web_search_tool_result_captured_in_provider_specific_fields(): assert ( web_search_results[0]["content"][0]["title"] == "Fun Otter Facts" ), "First result title should match" + + +def test_container_in_provider_specific_fields_streaming(): + """ + Test that container is captured in provider_specific_fields for streaming responses. + + When container with skills is used, the container field should be present in + the provider_specific_fields of the message_delta chunk. + """ + iterator = ModelResponseIterator( + streaming_response=MagicMock(), sync_stream=True, json_mode=False + ) + + # Simulate streaming chunks + chunks = [ + # 1. message_start + { + "type": "message_start", + "message": { + "id": "msg_123", + "type": "message", + "role": "assistant", + "content": [], + "usage": {"input_tokens": 98976, "output_tokens": 1}, + }, + }, + # 2. content_block_start for text + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "text", + "text": "", + }, + }, + # 3. content_block_delta with text + { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": "Hello, this is a response"}, + }, + # 4. content_block_stop for text + {"type": "content_block_stop", "index": 0}, + # 5. message_delta with container - THIS IS WHAT WE'RE TESTING + { + "type": "message_delta", + "delta": { + "stop_reason": "end_turn", + "stop_sequence": None, + "container": { + "id": "container_011CW9hA9zpZ8xD3bjjShy4p", + "expires_at": "2025-12-16T04:57:16.913181Z", + "skills": [ + { + "type": "anthropic", + "skill_id": "pptx", + "version": "20251013", + } + ], + }, + }, + "usage": { + "input_tokens": 98976, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 931, + "server_tool_use": {"web_search_requests": 0}, + }, + }, + ] + + container_field = None + for chunk in chunks: + parsed = iterator.chunk_parser(chunk) + if ( + parsed.choices + and parsed.choices[0].delta.provider_specific_fields + and "container" in parsed.choices[0].delta.provider_specific_fields + ): + container_field = parsed.choices[0].delta.provider_specific_fields[ + "container" + ] + + # Verify container was captured + assert container_field is not None, "container should be captured in provider_specific_fields" + assert ( + container_field["id"] == "container_011CW9hA9zpZ8xD3bjjShy4p" + ), "container id should match" + assert ( + container_field["expires_at"] == "2025-12-16T04:57:16.913181Z" + ), "expires_at should match" + assert len(container_field["skills"]) == 1, "Should have 1 skill" + assert ( + container_field["skills"][0]["skill_id"] == "pptx" + ), "skill_id should be pptx" + assert ( + container_field["skills"][0]["version"] == "20251013" + ), "version should match" + + +def test_container_in_provider_specific_fields_non_streaming(): + """ + Test that container is captured in provider_specific_fields for non-streaming responses. + + When container with skills is used in non-streaming, the container field should be + present in the provider_specific_fields of the response. + """ + iterator = ModelResponseIterator( + streaming_response=MagicMock(), sync_stream=False, json_mode=False + ) + + # Simulate a message_delta chunk with container (as it would appear in non-streaming) + message_delta_chunk = { + "type": "message_delta", + "delta": { + "stop_reason": "end_turn", + "stop_sequence": None, + "container": { + "id": "container_abc123xyz", + "expires_at": "2025-12-20T10:30:00.000000Z", + "skills": [ + { + "type": "anthropic", + "skill_id": "code_execution", + "version": "latest", + }, + { + "type": "anthropic", + "skill_id": "pptx", + "version": "20251013", + }, + ], + }, + }, + "usage": { + "input_tokens": 1000, + "output_tokens": 200, + }, + } + + model_response = iterator.chunk_parser(message_delta_chunk) + + # Verify container is in provider_specific_fields + assert model_response.choices[0].delta.provider_specific_fields is not None + assert "container" in model_response.choices[0].delta.provider_specific_fields + container_field = model_response.choices[0].delta.provider_specific_fields[ + "container" + ] + + assert container_field["id"] == "container_abc123xyz", "container id should match" + assert ( + container_field["expires_at"] == "2025-12-20T10:30:00.000000Z" + ), "expires_at should match" + assert len(container_field["skills"]) == 2, "Should have 2 skills" + assert ( + container_field["skills"][0]["skill_id"] == "code_execution" + ), "First skill_id should be code_execution" + assert ( + container_field["skills"][1]["skill_id"] == "pptx" + ), "Second skill_id should be pptx" + + +def test_container_absent_when_not_provided(): + """ + Test that container is not added to provider_specific_fields when not provided. + + This ensures we don't add empty or None container fields. + """ + iterator = ModelResponseIterator( + streaming_response=MagicMock(), sync_stream=False, json_mode=False + ) + + # message_delta without container + message_delta_chunk = { + "type": "message_delta", + "delta": { + "stop_reason": "end_turn", + "stop_sequence": None, + }, + "usage": { + "input_tokens": 1000, + "output_tokens": 200, + }, + } + + model_response = iterator.chunk_parser(message_delta_chunk) + + # Verify container is NOT in provider_specific_fields when not provided + if model_response.choices[0].delta.provider_specific_fields: + assert ( + "container" not in model_response.choices[0].delta.provider_specific_fields + ), "container should not be present when not provided in delta" From f23f15755676ad2b8e9223626b7e938d8b8d19f0 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Tue, 16 Dec 2025 21:27:42 +0530 Subject: [PATCH 5/5] fix error: Incompatible types (expression has type dict[Any, Any] --- litellm/llms/anthropic/chat/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 1f86e107d0..d0ec36e68f 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -697,7 +697,7 @@ class ModelResponseIterator: type="function", function=ChatCompletionToolCallFunctionChunk( name=content_block_start["content_block"]["name"], - arguments=content_block_start["content_block"]["input"], + arguments=str(content_block_start["content_block"]["input"]), ), index=self.tool_index, )