{"record":{"id":"d3d1b063fc746ba0","repo":"BerriAI/litellm","slug":"skill-not-found-skill-id","errorCode":null,"errorMessage":"Skill not found: {skill_id}","messagePattern":"Skill not found: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/llms/litellm_proxy/skills/handler.py","lineNumber":159,"sourceCode":"            return cached\n\n        prisma_client: Final = await LiteLLMSkillsHandler._get_prisma_client()\n        skill: Final = await SkillsRepository(prisma_client).table.find_unique(where={\"skill_id\": skill_id})\n        _SKILL_CACHE.set_cache(skill_id, skill if skill is not None else _NEGATIVE_SKILL_SENTINEL)\n        return skill\n\n    @staticmethod\n    async def get_skill(\n        skill_id: str,\n        user_api_key_dict: UserAPIKeyAuth | None = None,\n    ) -> LiteLLM_SkillsTable:\n        verbose_logger.debug(\"LiteLLMSkillsHandler: Getting skill %s\", skill_id)\n\n        skill: Final = await LiteLLMSkillsHandler._load_skill(skill_id)\n        # Same \"not found\" message for both \"missing\" and \"cross-tenant\"\n        # so callers can't enumerate skill IDs they don't own.\n        if skill is None or not user_can_access_resource_owner(getattr(skill, \"created_by\", None), user_api_key_dict):\n            raise ValueError(f\"Skill not found: {skill_id}\")\n\n        return _prisma_skill_to_litellm(skill)\n\n    @staticmethod\n    async def delete_skill(\n        skill_id: str,\n        user_api_key_dict: UserAPIKeyAuth | None = None,\n    ) -> dict[str, str]:\n        prisma_client: Final = await LiteLLMSkillsHandler._get_prisma_client()\n        verbose_logger.debug(\"LiteLLMSkillsHandler: Deleting skill %s\", skill_id)\n\n        skill: Final = await LiteLLMSkillsHandler._load_skill(skill_id)\n        if skill is None or not user_can_access_resource_owner(getattr(skill, \"created_by\", None), user_api_key_dict):\n            raise ValueError(f\"Skill not found: {skill_id}\")\n\n        await SkillsRepository(prisma_client).table.delete(where={\"skill_id\": skill_id})\n        _SKILL_CACHE.set_cache(skill_id, _NEGATIVE_SKILL_SENTINEL)\n","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/llms/litellm_proxy/skills/handler.py#L141-L177","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"# before\nskill = await LiteLLMSkillsHandler.get_skill(\"skill/others-skill\", user_api_key_dict=auth)  # ValueError\n\n# after\ntry:\n    skill = await LiteLLMSkillsHandler.get_skill(skill_id, user_api_key_dict=auth)\nexcept ValueError:\n    raise HTTPException(status_code=404, detail=f\"Skill not found: {skill_id}\")  # same message, no enumeration","handlingStrategy":"try-catch","validationCode":"# cheap pre-check when the caller may hold a stale id\nskills = await LiteLLMSkillsHandler.list_skills_for_caller(user_api_key_dict=auth)  # if available\nwanted = {s.skill_id for s in skills}\nif skill_id not in wanted:\n    raise HTTPException(status_code=404, detail=f\"Skill not found: {skill_id}\")","typeGuard":null,"tryCatchPattern":"try:\n    skill = await LiteLLMSkillsHandler.get_skill(skill_id, user_api_key_dict=auth)\nexcept ValueError:\n    # identical message for missing vs cross-tenant: do NOT differentiate to clients\n    raise HTTPException(status_code=404, detail=f\"Skill not found: {skill_id}\")","preventionTips":["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"],"tags":["litellm-proxy","skills","not-found","authorization","multi-tenant"],"backgroundTag":"resource-not-found","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}