diff --git a/docs/my-website/docs/providers/anthropic.md b/docs/my-website/docs/providers/anthropic.md index f78af51bd90..bcfb698a0f8 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 01d7f076edc..2a7f8aa3ddf 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 e96d6cc61a9..41febd4920a 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"