litellm/tests/unit/skills/test_skills_db.py
devin-ai-integration[bot] e0398455d0
test: migrate tests/llm_translation into unit, integration and e2e tiers (#45759)
* test: migrate selected llm translation contracts

* test: migrate anthropic completion translation tests

* test: migrate bedrock and cohere translation tests

* test: tighten bedrock translation migration

* test: migrate azure and openai model translation tests

* test: migrate openai and small-provider translation tests

* test: migrate gemini and vertex translation tests

* test: migrate optional params and rerank translation tests

* test: migrate huggingface, json and openrouter translation tests

* test: fix openrouter integration expectations

* test: migrate databricks, xai, cometapi and fireworks translation tests

* test: migrate ovhcloud, prompt and small provider translation tests

* test: split reasoning effort grid into unit and e2e

* test: migrate router and realtime guardrail translation tests

* test: migrate google interactions translation tests

* test: migrate a2a, evals, skills and hosted vllm translation tests

* test: remove remaining llm translation suite

* test: tighten migrated llm translation unit tests and land missing rows

Restores rows the first pass dropped or weakened (bedrock agentcore and openwebui thinking stream, anthropic prefix, citations and tool_choice, azure agents handler, huggingface bare api_base, optional params num_retries, alternating roles, rerank response type), replaces truthiness and call-count checks with whole-value assertions, and moves tests to the mirror file of the code they cover

* test: move cohere, databricks, fireworks and gemini rows to mirror files over mocked HTTP

Restores three TestCohereV2Transform methods the first pass nested out of collection, replaces direct transform calls with respx-backed completion calls, and drops the databricks and fireworks files that sat outside their mirror paths

* test: close lost-intent gaps found in review of the llm translation migration

Points the hosted-mock guard at the triton test's new integration path, restores exact rerank result assertions for infinity, drops the reasoning e2e fallback that passed for any multi-line answer, and adds the mixtral invoke system prompt variants, eval deletion, and async pdf url download that the legacy suite exercised

* test: assert provider acceptance in the reasoning effort e2e

The live half of the split grid proves each route accepts reasoning_effort. claude-fable-5-1 on the direct route answers without surfacing reasoning content, so a reasoning-presence check either fails on head or needs a fallback that passes for any answer. Wire shape stays covered by the unit grid

* test: cover async and stream-end paths the legacy translation suite exercised

Adds async variants for bedrock converse file and thinking content, bedrock embedding timeout and region resolution, interaction stream termination and success logging, xai reasoning token folding, and bytes chunks with a prefix before data:. Also scopes the vertex safety settings global in test_vertex_safety_settings with monkeypatch so it stops leaking into gemini request bodies on the same worker

* test: type the new llm translation e2e calls and mark moved fixture dirs as packages

* test: name the reasoning effort e2e models as module constants

* test: assert reasoning content, register the new gemini coverage ids and give the guardrail config e2e a unique name

* test: block the prompt the shared guardrail denies and drop the reasoning presence check

* test: drop the duplicate litellm import left by the main sync

---------

Co-authored-by: yuneng <yuneng@berri.ai>
2026-10-11 07:58:53 +00:00

260 lines
7.7 KiB
Python

"""
Test LiteLLM Skills SDK with custom_llm_provider=litellm_proxy
Tests the SDK-level skills methods when using the LiteLLM database backend:
1. Create a skill using SDK and verify it was stored correctly
2. List skills using SDK
3. Get a skill by ID using SDK
4. Delete a skill using SDK
5. Skills injection hook correctly resolves skills from database
"""
import os
import zipfile
from contextlib import contextmanager
from io import BytesIO
from pathlib import Path
import pytest
import litellm
from litellm.caching.caching import DualCache
from litellm.proxy import proxy_server
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.utils import PrismaClient, ProxyLogging
from litellm.types.utils import LlmProviders
import openai
proxy_logging_obj = ProxyLogging(user_api_key_cache=DualCache())
@contextmanager
def create_skill_zip(skill_name: str):
"""
Helper context manager to create a zip file for a skill.
Args:
skill_name: Name of the skill directory in test_skills_data/
Yields:
Tuple of (file handle, file content bytes)
The zip file is automatically cleaned up after use.
"""
test_dir = Path(__file__).parent / "test_skills_data"
skill_dir = test_dir / skill_name
# Create a zip file containing the skill directory
zip_path = test_dir / f"{skill_name}.zip"
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
zip_file.write(skill_dir, arcname=skill_name)
zip_file.write(skill_dir / "SKILL.md", arcname=f"{skill_name}/SKILL.md")
try:
with open(zip_path, "rb") as f:
content = f.read()
f.seek(0)
yield f, content
finally:
# Clean up zip file
if zip_path.exists():
zip_path.unlink()
@pytest.fixture
def prisma_client():
"""Set up prisma client for tests."""
from litellm.proxy.proxy_cli import append_query_params
params = {"connection_limit": 100, "pool_timeout": 60}
database_url = os.getenv("DATABASE_URL")
modified_url = append_query_params(database_url, params)
os.environ["DATABASE_URL"] = modified_url
prisma_client = PrismaClient(
database_url=os.environ["DATABASE_URL"], proxy_logging_obj=proxy_logging_obj
)
return prisma_client
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
@pytest.mark.asyncio
async def test_create_skill_sdk(prisma_client):
"""
Test creating a skill using SDK with custom_llm_provider=litellm_proxy.
Verifies that:
- Skill is created with correct display_title
- Skill ID is generated and returned
- Skill response has correct type
"""
setattr(proxy_server, "prisma_client", prisma_client)
await proxy_server.prisma_client.connect()
from litellm.skills.main import acreate_skill, adelete_skill
# Create a skill using SDK
skill = await acreate_skill(
display_title="SDK Test Skill",
extra_body={
"description": "A test skill created via SDK",
"instructions": "Use this skill for SDK testing",
},
custom_llm_provider=LlmProviders.LITELLM_PROXY.value,
)
# Verify skill was created correctly
assert skill is not None
assert skill.id is not None
assert skill.id.startswith("litellm_skill")
assert skill.display_title == "SDK Test Skill"
assert skill.type == "skill"
assert skill.source == "custom"
# Clean up
await adelete_skill(
skill_id=skill.id,
custom_llm_provider=LlmProviders.LITELLM_PROXY.value,
)
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
@pytest.mark.asyncio
async def test_list_skills_sdk(prisma_client):
"""
Test listing skills using SDK with custom_llm_provider=litellm_proxy.
Verifies that:
- Multiple skills can be created
- List returns the created skills
"""
setattr(proxy_server, "prisma_client", prisma_client)
await proxy_server.prisma_client.connect()
from litellm.skills.main import acreate_skill, adelete_skill, alist_skills
# Create multiple skills
created_skill_ids = []
for i in range(3):
skill = await acreate_skill(
display_title=f"List Test Skill {i}",
extra_body={
"description": f"Test skill {i} for list test",
},
custom_llm_provider=LlmProviders.LITELLM_PROXY.value,
)
created_skill_ids.append(skill.id)
# List skills using SDK
response = await alist_skills(
limit=10,
custom_llm_provider=LlmProviders.LITELLM_PROXY.value,
)
# Verify we got skills back
assert response is not None
assert response.data is not None
assert len(response.data) >= 3
# Verify our created skills are in the list
skill_ids_in_list = [s.id for s in response.data]
for created_id in created_skill_ids:
assert created_id in skill_ids_in_list
# Clean up
for skill_id in created_skill_ids:
await adelete_skill(
skill_id=skill_id,
custom_llm_provider=LlmProviders.LITELLM_PROXY.value,
)
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
@pytest.mark.asyncio
async def test_get_skill_sdk(prisma_client):
"""
Test getting a skill by ID using SDK with custom_llm_provider=litellm_proxy.
Verifies that:
- Skill can be retrieved by ID
- Retrieved skill has correct data
"""
setattr(proxy_server, "prisma_client", prisma_client)
await proxy_server.prisma_client.connect()
from litellm.skills.main import acreate_skill, adelete_skill, aget_skill
# Create a skill
created_skill = await acreate_skill(
display_title="Get Test Skill",
extra_body={
"description": "A skill for get test",
},
custom_llm_provider=LlmProviders.LITELLM_PROXY.value,
)
# Get the skill by ID using SDK
retrieved_skill = await aget_skill(
skill_id=created_skill.id,
custom_llm_provider=LlmProviders.LITELLM_PROXY.value,
)
# Verify retrieved skill matches created skill
assert retrieved_skill is not None
assert retrieved_skill.id == created_skill.id
assert retrieved_skill.display_title == "Get Test Skill"
# Clean up
await adelete_skill(
skill_id=created_skill.id,
custom_llm_provider=LlmProviders.LITELLM_PROXY.value,
)
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
@pytest.mark.asyncio
async def test_delete_skill_sdk(prisma_client):
"""
Test deleting a skill using SDK with custom_llm_provider=litellm_proxy.
Verifies that:
- Skill can be deleted by ID
- Deleted skill cannot be retrieved
"""
setattr(proxy_server, "prisma_client", prisma_client)
await proxy_server.prisma_client.connect()
from litellm.skills.main import acreate_skill, adelete_skill, aget_skill
# Create a skill
created_skill = await acreate_skill(
display_title="Delete Test Skill",
extra_body={
"description": "A skill to be deleted",
},
custom_llm_provider=LlmProviders.LITELLM_PROXY.value,
)
# Verify skill exists
retrieved = await aget_skill(
skill_id=created_skill.id,
custom_llm_provider=LlmProviders.LITELLM_PROXY.value,
)
assert retrieved is not None
# Delete the skill using SDK
result = await adelete_skill(
skill_id=created_skill.id,
custom_llm_provider=LlmProviders.LITELLM_PROXY.value,
)
assert result.id == created_skill.id
assert result.type == "skill_deleted"
# Verify skill no longer exists
with pytest.raises(openai.APIError):
await aget_skill(
skill_id=created_skill.id,
custom_llm_provider=LlmProviders.LITELLM_PROXY.value,
)