[Feat] WatsonX - allow passing zen_api_key dynamically (#16655)

* test_watsonx_zen_api_key_from_client

* zen api key

* docs using zen api key
This commit is contained in:
Ishaan Jaff
2025-12-01 12:55:47 -08:00
committed by GitHub
parent a73bd751fc
commit ce0dc0c8b9
4 changed files with 422 additions and 13 deletions
@@ -175,3 +175,56 @@ For all available models, see [watsonx.ai documentation](https://dataplatform.cl
For all available embedding models, see [watsonx.ai embedding documentation](https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-models-embed.html?context=wx).
## Advanced
### Using Zen API Key
You can use a Zen API key for long-term authentication instead of generating IAM tokens. Pass it either as an environment variable or as a parameter:
```python
import os
from litellm import completion
# Option 1: Set as environment variable
os.environ["WATSONX_ZENAPIKEY"] = "your-zen-api-key"
response = completion(
model="watsonx/ibm/granite-13b-chat-v2",
messages=[{"content": "What is your favorite color?", "role": "user"}],
project_id="your-project-id"
)
# Option 2: Pass as parameter
response = completion(
model="watsonx/ibm/granite-13b-chat-v2",
messages=[{"content": "What is your favorite color?", "role": "user"}],
zen_api_key="your-zen-api-key",
project_id="your-project-id"
)
```
**Using with LiteLLM Proxy via OpenAI client:**
```python
import openai
client = openai.OpenAI(
api_key="sk-1234", # LiteLLM proxy key
base_url="http://0.0.0.0:4000"
)
response = client.chat.completions.create(
model="watsonx/ibm/granite-3-3-8b-instruct",
messages=[{"role": "user", "content": "What is your favorite color?"}],
max_tokens=2048,
extra_body={
"project_id": "your-project-id",
"zen_api_key": "your-zen-api-key"
}
)
```
See [IBM documentation](https://www.ibm.com/docs/en/watsonx/w-and-w/2.2.0?topic=keys-generating-zenapikey-authorization-tokens) for more information on generating Zen API keys.
+274 -12
View File
@@ -1,17 +1,279 @@
# Anthropic Skills API
# Anthropic Skills API Integration
This folder maintains the integration for the Anthropic Skills API.
This module provides comprehensive support for the Anthropic Skills API through LiteLLM.
You can do the following with the Anthropic Skills API:
## Features
1. Create a new skill
2. List all skills
3. Get a skill
4. Delete a skill
The Skills API allows you to:
- **Create skills**: Define reusable AI capabilities
- **List skills**: Browse all available skills
- **Get skills**: Retrieve detailed information about a specific skill
- **Delete skills**: Remove skills that are no longer needed
## Quick Start
Versions:
- Create Skill Version
- List Skill Versions
- Get Skill Version
- Delete Skill Version
### Prerequisites
Set your Anthropic API key:
```python
import os
os.environ["ANTHROPIC_API_KEY"] = "your-api-key-here"
```
### Basic Usage
#### Create a Skill
```python
import litellm
# Create a skill with files
# Note: All files must be in the same top-level directory
# and must include a SKILL.md file at the root
skill = litellm.create_skill(
files=[
# List of file objects to upload
# Must include SKILL.md
],
display_title="Python Code Generator",
custom_llm_provider="anthropic"
)
print(f"Created skill: {skill.id}")
# Asynchronous version
skill = await litellm.acreate_skill(
files=[...], # Your files here
display_title="Python Code Generator",
custom_llm_provider="anthropic"
)
```
#### List Skills
```python
# List all skills
skills = litellm.list_skills(
custom_llm_provider="anthropic"
)
for skill in skills.data:
print(f"{skill.display_title}: {skill.id}")
# With pagination and filtering
skills = litellm.list_skills(
limit=20,
source="custom", # Filter by 'custom' or 'anthropic'
custom_llm_provider="anthropic"
)
# Get next page if available
if skills.has_more:
next_page = litellm.list_skills(
page=skills.next_page,
custom_llm_provider="anthropic"
)
```
#### Get a Skill
```python
skill = litellm.get_skill(
skill_id="skill_abc123",
custom_llm_provider="anthropic"
)
print(f"Skill: {skill.display_title}")
print(f"Created: {skill.created_at}")
print(f"Latest version: {skill.latest_version}")
print(f"Source: {skill.source}")
```
#### Delete a Skill
```python
result = litellm.delete_skill(
skill_id="skill_abc123",
custom_llm_provider="anthropic"
)
print(f"Deleted skill {result.id}, type: {result.type}")
```
## API Reference
### `create_skill()`
Create a new skill.
**Parameters:**
- `files` (List[Any], optional): Files to upload for the skill. All files must be in the same top-level directory and must include a SKILL.md file at the root.
- `display_title` (str, optional): Display title for the skill
- `custom_llm_provider` (str, optional): Provider name (default: "anthropic")
- `extra_headers` (dict, optional): Additional HTTP headers
- `timeout` (float, optional): Request timeout
**Returns:**
- `Skill`: The created skill object
**Async version:** `acreate_skill()`
### `list_skills()`
List all skills.
**Parameters:**
- `limit` (int, optional): Number of results to return per page (max 100, default 20)
- `page` (str, optional): Pagination token for fetching a specific page of results
- `source` (str, optional): Filter skills by source ('custom' or 'anthropic')
- `custom_llm_provider` (str, optional): Provider name (default: "anthropic")
- `extra_headers` (dict, optional): Additional HTTP headers
- `timeout` (float, optional): Request timeout
**Returns:**
- `ListSkillsResponse`: Object containing a list of skills and pagination info
**Async version:** `alist_skills()`
### `get_skill()`
Get a specific skill by ID.
**Parameters:**
- `skill_id` (str, required): The skill ID
- `custom_llm_provider` (str, optional): Provider name (default: "anthropic")
- `extra_headers` (dict, optional): Additional HTTP headers
- `timeout` (float, optional): Request timeout
**Returns:**
- `Skill`: The requested skill object
**Async version:** `aget_skill()`
### `delete_skill()`
Delete a skill.
**Parameters:**
- `skill_id` (str, required): The skill ID to delete
- `custom_llm_provider` (str, optional): Provider name (default: "anthropic")
- `extra_headers` (dict, optional): Additional HTTP headers
- `timeout` (float, optional): Request timeout
**Returns:**
- `DeleteSkillResponse`: Object with `id` and `type` fields
**Async version:** `adelete_skill()`
## Response Types
### `Skill`
Represents a skill from the Anthropic Skills API.
**Fields:**
- `id` (str): Unique identifier
- `created_at` (str): ISO 8601 timestamp
- `display_title` (str, optional): Display title
- `latest_version` (str, optional): Latest version identifier
- `source` (str): Source ("custom" or "anthropic")
- `type` (str): Object type (always "skill")
- `updated_at` (str): ISO 8601 timestamp
### `ListSkillsResponse`
Response from listing skills.
**Fields:**
- `data` (List[Skill]): List of skills
- `next_page` (str, optional): Pagination token for the next page
- `has_more` (bool): Whether more skills are available
### `DeleteSkillResponse`
Response from deleting a skill.
**Fields:**
- `id` (str): The deleted skill ID
- `type` (str): Deleted object type (always "skill_deleted")
## Architecture
The Skills API implementation follows LiteLLM's standard patterns:
1. **Type Definitions** (`litellm/types/llms/anthropic_skills.py`)
- Pydantic models for request/response types
- TypedDict definitions for request parameters
2. **Base Configuration** (`litellm/llms/base_llm/skills/transformation.py`)
- Abstract base class `BaseSkillsAPIConfig`
- Defines transformation interface for provider-specific implementations
3. **Provider Implementation** (`litellm/llms/anthropic/skills/transformation.py`)
- `AnthropicSkillsConfig` - Anthropic-specific transformations
- Handles API authentication, URL construction, and response mapping
4. **Main Handler** (`litellm/skills/main.py`)
- Public API functions (sync and async)
- Request validation and routing
- Error handling
5. **HTTP Handlers** (`litellm/llms/custom_httpx/llm_http_handler.py`)
- Low-level HTTP request/response handling
- Connection pooling and retry logic
## Beta API Support
The Skills API is in beta. The beta header (`skills-2025-10-02`) is automatically added by the Anthropic provider configuration. You can customize it if needed:
```python
skill = litellm.create_skill(
display_title="My Skill",
extra_headers={
"anthropic-beta": "skills-2025-10-02" # Or any other beta version
},
custom_llm_provider="anthropic"
)
```
The default beta version is configured in `litellm.constants.ANTHROPIC_SKILLS_API_BETA_VERSION`.
## Error Handling
All Skills API functions follow LiteLLM's standard error handling:
```python
import litellm
try:
skill = litellm.create_skill(
display_title="My Skill",
custom_llm_provider="anthropic"
)
except litellm.exceptions.AuthenticationError as e:
print(f"Authentication failed: {e}")
except litellm.exceptions.RateLimitError as e:
print(f"Rate limit exceeded: {e}")
except litellm.exceptions.APIError as e:
print(f"API error: {e}")
```
## Contributing
To add support for Skills API to a new provider:
1. Create provider-specific configuration class inheriting from `BaseSkillsAPIConfig`
2. Implement all abstract methods for request/response transformations
3. Register the config in `ProviderConfigManager.get_provider_skills_api_config()`
4. Add appropriate tests
## Related Documentation
- [Anthropic Skills API Documentation](https://platform.claude.com/docs/en/api/beta/skills/create)
- [LiteLLM Responses API](../../../responses/)
- [Provider Configuration System](../../base_llm/)
## Support
For issues or questions:
- GitHub Issues: https://github.com/BerriAI/litellm/issues
- Discord: https://discord.gg/wuPM9dRgDw
+5 -1
View File
@@ -252,9 +252,13 @@ class IBMWatsonXMixin:
Optional[str],
optional_params.get("token") or get_secret_str("WATSONX_TOKEN"),
)
zen_api_key = cast(
Optional[str],
optional_params.pop("zen_api_key", None) or get_secret_str("WATSONX_ZENAPIKEY"),
)
if token:
headers["Authorization"] = f"Bearer {token}"
elif zen_api_key := get_secret_str("WATSONX_ZENAPIKEY"):
elif zen_api_key:
headers["Authorization"] = f"ZenApiKey {zen_api_key}"
else:
token = _generate_watsonx_token(api_key=api_key, token=token)
@@ -414,3 +414,93 @@ def test_watsonx_chat_completion_with_reasoning_effort(monkeypatch):
assert (
json_data["reasoning_effort"] == "low"
), "The value of 'reasoning_effort' should be 'low'."
def test_watsonx_zen_api_key_from_client(monkeypatch, watsonx_chat_completion_call):
"""
Test that zen_api_key can be passed from client code and is used in Authorization header.
"""
monkeypatch.setenv("WATSONX_PROJECT_ID", "test-project-id")
monkeypatch.setenv("WATSONX_API_BASE", "https://test-api.watsonx.ai")
model = "watsonx/ibm/granite-3-3-8b-instruct"
messages = [{"role": "user", "content": "What is your favorite color?"}]
client = HTTPHandler()
zen_api_key = "U1ZDLWQo="
# No need to patch token call since zen_api_key should skip token generation
with patch.object(client, "post") as mock_post:
try:
completion(
model=model,
messages=messages,
api_key="test_api_key",
client=client,
zen_api_key=zen_api_key,
)
except Exception as e:
print(f"Caught expected exception: {e}")
# Verify the request was made
assert mock_post.call_count == 1, "The completion endpoint should have been called once."
# Get the headers sent in the POST request
request_kwargs = mock_post.call_args.kwargs
headers = request_kwargs["headers"]
print("\nHeaders sent to WatsonX API:")
print(json.dumps(dict(headers), indent=2))
# Verify Authorization header uses ZenApiKey format
assert "Authorization" in headers, "Authorization header should be present."
assert headers["Authorization"] == f"ZenApiKey {zen_api_key}", (
f"Authorization header should use ZenApiKey format. "
f"Expected: 'ZenApiKey {zen_api_key}', Got: '{headers['Authorization']}'"
)
def test_watsonx_zen_api_key_from_env(monkeypatch, watsonx_chat_completion_call):
"""
Test that zen_api_key from environment variable is used in Authorization header.
"""
monkeypatch.setenv("WATSONX_PROJECT_ID", "test-project-id")
monkeypatch.setenv("WATSONX_API_BASE", "https://test-api.watsonx.ai")
zen_api_key = "U1ZDLWxpdG--==="
monkeypatch.setenv("WATSONX_ZENAPIKEY", zen_api_key)
model = "watsonx/ibm/granite-3-3-8b-instruct"
messages = [{"role": "user", "content": "What is your favorite color?"}]
client = HTTPHandler()
# No need to patch token call since zen_api_key should skip token generation
with patch.object(client, "post") as mock_post:
try:
completion(
model=model,
messages=messages,
api_key="test_api_key",
client=client,
)
except Exception as e:
print(f"Caught expected exception: {e}")
# Verify the request was made
assert mock_post.call_count == 1, "The completion endpoint should have been called once."
# Get the headers sent in the POST request
request_kwargs = mock_post.call_args.kwargs
headers = request_kwargs["headers"]
print("\nHeaders sent to WatsonX API:")
print(json.dumps(dict(headers), indent=2))
# Verify Authorization header uses ZenApiKey format
assert "Authorization" in headers, "Authorization header should be present."
assert headers["Authorization"] == f"ZenApiKey {zen_api_key}", (
f"Authorization header should use ZenApiKey format. "
f"Expected: 'ZenApiKey {zen_api_key}', Got: '{headers['Authorization']}'"
)