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

  1. Re-check the skill_id for typos and that it includes the full id
  2. Confirm the caller's identity scope (user/team/org/api key) matches the skill's created_by owner
  3. List the skills your own caller can see to verify the id exists under your scope
  4. 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

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


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)