382 lines
11 KiB
Markdown
382 lines
11 KiB
Markdown
|
|
# LiteLLM Skills - Database-Backed Skills Storage
|
||
|
|
|
||
|
|
This module provides database-backed skills storage as an alternative to Anthropic's cloud-based Skills API. It enables using skills with **any LLM provider** (Bedrock, OpenAI, Azure, etc.) by storing skills locally and converting them to tools + system prompt injection.
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
flowchart TB
|
||
|
|
subgraph "Skill Creation"
|
||
|
|
A[User creates skill with ZIP file] --> B{custom_llm_provider?}
|
||
|
|
B -->|anthropic| C[Forward to Anthropic API]
|
||
|
|
B -->|litellm_proxy| D[Store in LiteLLM Database]
|
||
|
|
|
||
|
|
D --> E[Extract & store:<br/>- display_title<br/>- description<br/>- instructions<br/>- file_content ZIP]
|
||
|
|
end
|
||
|
|
|
||
|
|
subgraph "Skill Usage in Messages API"
|
||
|
|
F[Request with container.skills] --> G[SkillsInjectionHook]
|
||
|
|
G --> H{skill_id prefix?}
|
||
|
|
|
||
|
|
H -->|"litellm:skill_abc"| I[Fetch from LiteLLM DB]
|
||
|
|
H -->|"skill_xyz" no prefix| J[Pass to Anthropic as native skill]
|
||
|
|
|
||
|
|
I --> K{Model provider?}
|
||
|
|
K -->|Anthropic API| L[Convert to tools]
|
||
|
|
K -->|Bedrock/OpenAI/etc| M[Convert to tools +<br/>Inject SKILL.md into system prompt]
|
||
|
|
|
||
|
|
J --> N[Keep in container.skills]
|
||
|
|
end
|
||
|
|
|
||
|
|
subgraph "Skill Resolution for Non-Anthropic"
|
||
|
|
M --> O[Extract SKILL.md from ZIP]
|
||
|
|
O --> P[Add to system prompt:<br/># Available Skills<br/>## Skill: My Skill<br/>SKILL.md content...]
|
||
|
|
P --> Q[Create OpenAI-style tool:<br/>type: function<br/>name: skill_id<br/>description: instructions]
|
||
|
|
Q --> R[Send to LLM Provider]
|
||
|
|
end
|
||
|
|
```
|
||
|
|
|
||
|
|
## Automatic Code Execution
|
||
|
|
|
||
|
|
For skills that include executable code (Python files), LiteLLM automatically handles:
|
||
|
|
|
||
|
|
1. **Pre-call hook** (`async_pre_call_hook`): Adds `litellm_code_execution` tool, injects SKILL.md content
|
||
|
|
2. **Post-call hook** (`async_post_call_success_deployment_hook`): Detects tool calls, executes code in Docker sandbox, continues loop
|
||
|
|
3. **Returns files**: Generated files (GIFs, images, etc.) returned directly on response
|
||
|
|
|
||
|
|
```mermaid
|
||
|
|
sequenceDiagram
|
||
|
|
participant User
|
||
|
|
participant LiteLLM as LiteLLM SDK
|
||
|
|
participant PreHook as async_pre_call_hook
|
||
|
|
participant LLM as LLM Provider
|
||
|
|
participant PostHook as async_post_call_success_deployment_hook
|
||
|
|
participant Sandbox as Docker Sandbox
|
||
|
|
|
||
|
|
User->>LiteLLM: litellm.acompletion(model, messages, container={skills: [...]})
|
||
|
|
|
||
|
|
Note over LiteLLM,PreHook: PRE-CALL HOOK
|
||
|
|
LiteLLM->>PreHook: Intercept request
|
||
|
|
PreHook->>PreHook: Fetch skill from DB (litellm:skill_id)
|
||
|
|
PreHook->>PreHook: Extract SKILL.md from ZIP
|
||
|
|
PreHook->>PreHook: Inject SKILL.md into system prompt
|
||
|
|
PreHook->>PreHook: Add litellm_code_execution tool
|
||
|
|
PreHook->>PreHook: Store skill files in metadata
|
||
|
|
PreHook-->>LiteLLM: Modified request
|
||
|
|
|
||
|
|
LiteLLM->>LLM: Forward to provider (OpenAI/Bedrock/etc)
|
||
|
|
LLM-->>LiteLLM: Response with tool_calls
|
||
|
|
|
||
|
|
Note over LiteLLM,PostHook: POST-CALL HOOK (Agentic Loop)
|
||
|
|
LiteLLM->>PostHook: Check response
|
||
|
|
|
||
|
|
loop Until no more tool calls
|
||
|
|
PostHook->>PostHook: Check for litellm_code_execution tool call
|
||
|
|
alt Has code execution tool call
|
||
|
|
PostHook->>Sandbox: Execute Python code
|
||
|
|
Sandbox->>Sandbox: Copy skill files to /sandbox
|
||
|
|
Sandbox->>Sandbox: Install requirements.txt
|
||
|
|
Sandbox->>Sandbox: Run code
|
||
|
|
Sandbox-->>PostHook: Result + generated files
|
||
|
|
PostHook->>PostHook: Add tool result to messages
|
||
|
|
PostHook->>LLM: Make another LLM call
|
||
|
|
LLM-->>PostHook: New response
|
||
|
|
else No code execution
|
||
|
|
PostHook->>PostHook: Break loop
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
PostHook->>PostHook: Attach files to response._litellm_generated_files
|
||
|
|
PostHook-->>LiteLLM: Modified response with files
|
||
|
|
LiteLLM-->>User: Final response with generated files
|
||
|
|
```
|
||
|
|
|
||
|
|
```python
|
||
|
|
import litellm
|
||
|
|
from litellm.proxy.hooks.litellm_skills import SkillsInjectionHook
|
||
|
|
|
||
|
|
# Register the hook (done once at startup)
|
||
|
|
hook = SkillsInjectionHook()
|
||
|
|
litellm.callbacks.append(hook)
|
||
|
|
|
||
|
|
# ONE request - LiteLLM handles everything automatically
|
||
|
|
# The container parameter triggers the SkillsInjectionHook
|
||
|
|
response = await litellm.acompletion(
|
||
|
|
model="gpt-4o-mini",
|
||
|
|
messages=[{"role": "user", "content": "Create a bouncing ball GIF"}],
|
||
|
|
container={
|
||
|
|
"skills": [{"type": "custom", "skill_id": "litellm:skill_abc123"}]
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
# Files are attached directly to response
|
||
|
|
generated_files = response._litellm_generated_files
|
||
|
|
for f in generated_files:
|
||
|
|
print(f"Generated: {f['name']} ({f['size']} bytes)")
|
||
|
|
# f['content_base64'] contains the file data
|
||
|
|
```
|
||
|
|
|
||
|
|
This mimics Anthropic's behavior - no manual agentic loop needed!
|
||
|
|
|
||
|
|
### How it works
|
||
|
|
|
||
|
|
The `SkillsInjectionHook` uses two hooks:
|
||
|
|
|
||
|
|
1. **`async_pre_call_hook`** (proxy only): Transforms the request before LLM call
|
||
|
|
- Fetches skills from DB
|
||
|
|
- Injects SKILL.md into system prompt
|
||
|
|
- Adds `litellm_code_execution` tool
|
||
|
|
- Sets `_litellm_code_execution_enabled=True` in metadata
|
||
|
|
|
||
|
|
2. **`async_post_call_success_deployment_hook`** (SDK + proxy): Called after LLM response
|
||
|
|
- Checks if response has `litellm_code_execution` tool call
|
||
|
|
- Executes code in Docker sandbox
|
||
|
|
- Adds result to messages, makes another LLM call
|
||
|
|
- Repeats until model gives final response
|
||
|
|
- Attaches generated files to `response._litellm_generated_files`
|
||
|
|
|
||
|
|
## File Structure
|
||
|
|
|
||
|
|
```
|
||
|
|
litellm/llms/litellm_proxy/skills/
|
||
|
|
├── __init__.py # Exports all skill components
|
||
|
|
├── handler.py # LiteLLMSkillsHandler - database CRUD operations (Prisma)
|
||
|
|
├── transformation.py # LiteLLMSkillsTransformationHandler - SDK transformation layer
|
||
|
|
├── prompt_injection.py # SkillPromptInjectionHandler - SKILL.md extraction and injection
|
||
|
|
├── sandbox_executor.py # SkillsSandboxExecutor - Docker sandbox code execution
|
||
|
|
├── code_execution.py # CodeExecutionHandler - automatic agentic loop
|
||
|
|
└── README.md # This file
|
||
|
|
|
||
|
|
litellm/proxy/hooks/litellm_skills/
|
||
|
|
├── __init__.py # Re-exports from SDK + SkillsInjectionHook
|
||
|
|
└── main.py # SkillsInjectionHook - CustomLogger hook for proxy
|
||
|
|
```
|
||
|
|
|
||
|
|
## Components
|
||
|
|
|
||
|
|
### 1. `handler.py` - LiteLLMSkillsHandler
|
||
|
|
|
||
|
|
Database operations for skills CRUD:
|
||
|
|
|
||
|
|
```python
|
||
|
|
from litellm.llms.litellm_proxy.skills import LiteLLMSkillsHandler
|
||
|
|
|
||
|
|
# Create skill
|
||
|
|
skill = await LiteLLMSkillsHandler.create_skill(
|
||
|
|
data=NewSkillRequest(
|
||
|
|
display_title="My Skill",
|
||
|
|
description="A helpful skill",
|
||
|
|
instructions="Use this skill when...",
|
||
|
|
file_content=zip_bytes, # ZIP file content
|
||
|
|
file_name="my-skill.zip",
|
||
|
|
file_type="application/zip",
|
||
|
|
),
|
||
|
|
user_id="user_123"
|
||
|
|
)
|
||
|
|
|
||
|
|
# List skills
|
||
|
|
skills = await LiteLLMSkillsHandler.list_skills(limit=10, offset=0)
|
||
|
|
|
||
|
|
# Get skill
|
||
|
|
skill = await LiteLLMSkillsHandler.get_skill(skill_id="skill_abc123")
|
||
|
|
|
||
|
|
# Delete skill
|
||
|
|
await LiteLLMSkillsHandler.delete_skill(skill_id="skill_abc123")
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. `transformation.py` - LiteLLMSkillsTransformationHandler
|
||
|
|
|
||
|
|
SDK-level transformation layer that wraps handler operations:
|
||
|
|
|
||
|
|
```python
|
||
|
|
from litellm.llms.litellm_proxy.skills import LiteLLMSkillsTransformationHandler
|
||
|
|
|
||
|
|
handler = LiteLLMSkillsTransformationHandler()
|
||
|
|
|
||
|
|
# Async create
|
||
|
|
skill = await handler.create_skill_handler(
|
||
|
|
display_title="My Skill",
|
||
|
|
files=[zip_file],
|
||
|
|
_is_async=True
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
## Skill ZIP Format
|
||
|
|
|
||
|
|
Skills must be packaged as ZIP files with a `SKILL.md` file:
|
||
|
|
|
||
|
|
```
|
||
|
|
my-skill.zip
|
||
|
|
└── my-skill/
|
||
|
|
└── SKILL.md
|
||
|
|
```
|
||
|
|
|
||
|
|
### SKILL.md Format
|
||
|
|
|
||
|
|
```markdown
|
||
|
|
---
|
||
|
|
name: my-skill
|
||
|
|
description: A brief description of what this skill does
|
||
|
|
---
|
||
|
|
|
||
|
|
# My Skill
|
||
|
|
|
||
|
|
Detailed instructions for the LLM on how to use this skill.
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
|
||
|
|
When the user asks about X, use this skill to...
|
||
|
|
|
||
|
|
## Examples
|
||
|
|
|
||
|
|
- Example 1: ...
|
||
|
|
- Example 2: ...
|
||
|
|
```
|
||
|
|
|
||
|
|
## SDK Usage
|
||
|
|
|
||
|
|
### Create Skill in LiteLLM Database
|
||
|
|
|
||
|
|
```python
|
||
|
|
import litellm
|
||
|
|
|
||
|
|
# Create skill stored in LiteLLM DB
|
||
|
|
skill = litellm.create_skill(
|
||
|
|
display_title="Data Analysis Skill",
|
||
|
|
files=[open("data-analysis.zip", "rb")],
|
||
|
|
custom_llm_provider="litellm_proxy", # Store in LiteLLM DB
|
||
|
|
)
|
||
|
|
|
||
|
|
print(f"Created skill: {skill.id}") # skill_abc123
|
||
|
|
```
|
||
|
|
|
||
|
|
### Use Skill with Any Provider
|
||
|
|
|
||
|
|
```python
|
||
|
|
import litellm
|
||
|
|
|
||
|
|
# Use LiteLLM-stored skill with Bedrock
|
||
|
|
response = litellm.completion(
|
||
|
|
model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0",
|
||
|
|
messages=[{"role": "user", "content": "Analyze this data..."}],
|
||
|
|
container={
|
||
|
|
"skills": [
|
||
|
|
{"type": "custom", "skill_id": "litellm:skill_abc123"} # litellm: prefix
|
||
|
|
]
|
||
|
|
}
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
## How Skill Resolution Works
|
||
|
|
|
||
|
|
### Step 1: Request with Skills
|
||
|
|
|
||
|
|
```python
|
||
|
|
{
|
||
|
|
"model": "bedrock/claude-3-sonnet",
|
||
|
|
"messages": [{"role": "user", "content": "Help me analyze data"}],
|
||
|
|
"container": {
|
||
|
|
"skills": [
|
||
|
|
{"type": "custom", "skill_id": "litellm:skill_abc123"}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 2: SkillsInjectionHook Processing
|
||
|
|
|
||
|
|
The hook (`litellm/proxy/hooks/litellm_skills/main.py`) intercepts the request:
|
||
|
|
|
||
|
|
1. **Detects `litellm:` prefix** → Fetches skill from database
|
||
|
|
2. **Checks model provider** → Bedrock is not Anthropic
|
||
|
|
3. **Extracts SKILL.md** from stored ZIP file
|
||
|
|
4. **Converts skill to tool** + **Injects content into system prompt**
|
||
|
|
|
||
|
|
### Step 3: Transformed Request
|
||
|
|
|
||
|
|
```python
|
||
|
|
{
|
||
|
|
"model": "bedrock/claude-3-sonnet",
|
||
|
|
"messages": [
|
||
|
|
{
|
||
|
|
"role": "system",
|
||
|
|
"content": """
|
||
|
|
---
|
||
|
|
|
||
|
|
# Available Skills
|
||
|
|
|
||
|
|
## Skill: Data Analysis Skill
|
||
|
|
|
||
|
|
# Data Analysis Skill
|
||
|
|
|
||
|
|
This skill helps with data analysis tasks...
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
When the user asks about data analysis...
|
||
|
|
"""
|
||
|
|
},
|
||
|
|
{"role": "user", "content": "Help me analyze data"}
|
||
|
|
],
|
||
|
|
"tools": [
|
||
|
|
{
|
||
|
|
"type": "function",
|
||
|
|
"function": {
|
||
|
|
"name": "skill_abc123",
|
||
|
|
"description": "This skill helps with data analysis tasks...",
|
||
|
|
"parameters": {"type": "object", "properties": {}, "required": []}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
# container is removed for non-Anthropic providers
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Database Schema
|
||
|
|
|
||
|
|
Skills are stored in `LiteLLM_SkillsTable`:
|
||
|
|
|
||
|
|
```prisma
|
||
|
|
model LiteLLM_SkillsTable {
|
||
|
|
skill_id String @id @default(uuid())
|
||
|
|
display_title String?
|
||
|
|
description String?
|
||
|
|
instructions String?
|
||
|
|
source String @default("custom")
|
||
|
|
latest_version String?
|
||
|
|
metadata Json? @default("{}")
|
||
|
|
file_content Bytes? // ZIP file binary content
|
||
|
|
file_name String? // Original filename
|
||
|
|
file_type String? // MIME type
|
||
|
|
created_at DateTime @default(now())
|
||
|
|
created_by String?
|
||
|
|
updated_at DateTime @default(now()) @updatedAt
|
||
|
|
updated_by String?
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Routing Summary
|
||
|
|
|
||
|
|
| Scenario | custom_llm_provider | skill_id Format | Behavior |
|
||
|
|
|----------|---------------------|-----------------|----------|
|
||
|
|
| Create skill on Anthropic | `anthropic` | N/A | Forward to Anthropic API |
|
||
|
|
| Create skill in LiteLLM DB | `litellm_proxy` | N/A | Store in database |
|
||
|
|
| Use Anthropic native skill | N/A | `skill_xyz` | Pass to Anthropic container.skills |
|
||
|
|
| Use LiteLLM skill on Anthropic | N/A | `litellm:skill_abc` | Convert to tools |
|
||
|
|
| Use LiteLLM skill on Bedrock/OpenAI | N/A | `litellm:skill_abc` | Convert to tools + inject SKILL.md |
|
||
|
|
|
||
|
|
## Testing
|
||
|
|
|
||
|
|
Run the tests:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
pytest tests/proxy_unit_tests/test_skills_db.py -v
|
||
|
|
```
|
||
|
|
|
||
|
|
Tests cover:
|
||
|
|
- Creating skills with file content
|
||
|
|
- Listing and retrieving skills
|
||
|
|
- Deleting skills
|
||
|
|
- Hook resolution with ZIP file extraction
|
||
|
|
- System prompt injection for non-Anthropic models
|
||
|
|
|