BerriAI/litellm · error · ValueError
Skill not found
Error message
Skill not found: {skill_id} What it means
get_skill raises ValueError('Skill not found: {skill_id}') for two indistinguishable-by-design cases: the skill_id does not exist, or it exists but the caller fails the user_can_access_resource_owner check against created_by (cross-tenant access). The identical message prevents callers from enumerating skill IDs they do not own. Note the loader caches results, including a negative sentinel for known-missing ids.
Solutions
- Re-check the skill_id for typos and that it includes the full id
- Confirm the caller's identity scope (user/team/org/api key) matches the skill's created_by owner
- List the skills your own caller can see to verify the id exists under your scope
- If ownership is genuinely wrong, have the proper owner recreate the skill or re-create it under the correct account
Example fix
# before
skill = await LiteLLMSkillsHandler.get_skill("skill/others-skill", user_api_key_dict=auth) # ValueError
# after
try:
skill = await LiteLLMSkillsHandler.get_skill(skill_id, user_api_key_dict=auth)
except ValueError:
raise HTTPException(status_code=404, detail=f"Skill not found: {skill_id}") # same message, no enumeration Defensive patterns
Strategy: try-catch
Validate before calling
# cheap pre-check when the caller may hold a stale id
skills = await LiteLLMSkillsHandler.list_skills_for_caller(user_api_key_dict=auth) # if available
wanted = {s.skill_id for s in skills}
if skill_id not in wanted:
raise HTTPException(status_code=404, detail=f"Skill not found: {skill_id}") Try / catch
try:
skill = await LiteLLMSkillsHandler.get_skill(skill_id, user_api_key_dict=auth)
except ValueError:
# identical message for missing vs cross-tenant: do NOT differentiate to clients
raise HTTPException(status_code=404, detail=f"Skill not found: {skill_id}") Prevention
- Copy skill_ids from a listing made by the same caller, never across environments or users
- Return a generic 404 to clients to preserve the anti-enumeration design
- Prefer fetch_skill_from_db in hooks — it returns None instead of raising for not-found
When it happens
Trigger: GET a skill with a typo'd or truncated skill_id; a user/team key requesting a skill created under a different user, team, or org; requesting a skill that was deleted (negative cache makes repeat lookups fail identically).
Common situations: Copying a skill_id between environments (dev vs prod); permissions changed after creation; the creator used a different key scope than the reader; skill deleted by its owner while consumers still reference it.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- Key not found
- Service-account keys cannot query user analytics. Use a…
- Unable to record skill ownership: caller has no identity…
- Admin-only endpoint. Not allowed to access this., your role=
- f"User not found
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/d3d1b063fc746ba0.
Report an issue: GitHub.
Appendix: source
Thrown at litellm/llms/litellm_proxy/skills/handler.py:159
return cached
prisma_client: Final = await LiteLLMSkillsHandler._get_prisma_client()
skill: Final = await SkillsRepository(prisma_client).table.find_unique(where={"skill_id": skill_id})
_SKILL_CACHE.set_cache(skill_id, skill if skill is not None else _NEGATIVE_SKILL_SENTINEL)
return skill
@staticmethod
async def get_skill(
skill_id: str,
user_api_key_dict: UserAPIKeyAuth | None = None,
) -> LiteLLM_SkillsTable:
verbose_logger.debug("LiteLLMSkillsHandler: Getting skill %s", skill_id)
skill: Final = await LiteLLMSkillsHandler._load_skill(skill_id)
# Same "not found" message for both "missing" and "cross-tenant"
# so callers can't enumerate skill IDs they don't own.
if skill is None or not user_can_access_resource_owner(getattr(skill, "created_by", None), user_api_key_dict):
raise ValueError(f"Skill not found: {skill_id}")
return _prisma_skill_to_litellm(skill)
@staticmethod
async def delete_skill(
skill_id: str,
user_api_key_dict: UserAPIKeyAuth | None = None,
) -> dict[str, str]:
prisma_client: Final = await LiteLLMSkillsHandler._get_prisma_client()
verbose_logger.debug("LiteLLMSkillsHandler: Deleting skill %s", skill_id)
skill: Final = await LiteLLMSkillsHandler._load_skill(skill_id)
if skill is None or not user_can_access_resource_owner(getattr(skill, "created_by", None), user_api_key_dict):
raise ValueError(f"Skill not found: {skill_id}")
await SkillsRepository(prisma_client).table.delete(where={"skill_id": skill_id})
_SKILL_CACHE.set_cache(skill_id, _NEGATIVE_SKILL_SENTINEL)
View on GitHub (pinned to 77b7c6c40c)