{"record":{"id":"81c4606f7912dbb3","repo":"BerriAI/litellm","slug":"unable-to-record-skill-ownership-caller-has-no-id","errorCode":null,"errorMessage":"Unable to record skill ownership: caller has no identity scope.","messagePattern":"Unable to record skill ownership: caller has no identity scope\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/llms/litellm_proxy/skills/handler.py","lineNumber":77,"sourceCode":"        return prisma_client\n\n    @staticmethod\n    async def create_skill(\n        data: NewSkillRequest,\n        user_id: str | None = None,\n        user_api_key_dict: UserAPIKeyAuth | None = None,\n    ) -> LiteLLM_SkillsTable:\n        prisma_client: Final = await LiteLLMSkillsHandler._get_prisma_client()\n\n        skill_id: Final = f\"{LITELLM_SKILL_ID_PREFIX}{uuid.uuid4()}\"\n        owner: Final = get_primary_resource_owner_scope(user_api_key_dict) or user_id\n        if owner is None:\n            # Identity-less callers (no user_id / team_id / org_id /\n            # api_key / token) can't be uniquely stamped on the row.\n            # Stamping a placeholder would let any two such callers see\n            # each other's skills via the shared owner. ValueError keeps\n            # this module FastAPI-free per the project layering rule.\n            raise ValueError(\"Unable to record skill ownership: caller has no identity scope.\")\n\n        skill_data: Final[dict[str, Any]] = {\n            \"skill_id\": skill_id,\n            \"display_title\": data.display_title,\n            \"description\": data.description,\n            \"instructions\": data.instructions,\n            \"source\": \"custom\",\n            \"created_by\": owner,\n            \"updated_by\": owner,\n        }\n\n        if data.metadata is not None:\n            from litellm.litellm_core_utils.safe_json_dumps import safe_dumps\n\n            skill_data[\"metadata\"] = safe_dumps(data.metadata)\n\n        if data.file_content is not None:\n            from prisma.fields import Base64","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/llms/litellm_proxy/skills/handler.py#L59-L95","documentation":"When creating a skill, the handler derives an owner from get_primary_resource_owner_scope(user_api_key_dict) (user/team/org/api-key identity) falling back to the explicit user_id. If both are absent — an identity-less caller — it raises ValueError rather than storing a placeholder, because a shared fake owner would let anonymous callers read each other's skills. This is a deliberate multi-tenant isolation guard.","triggerScenarios":"POSTing to the skills endpoint without an authenticated identity: missing/invalid API key header so user_api_key_dict carries no scopes, and no user_id supplied; internal/test invocations of create_skill passing neither argument.","commonSituations":"Calling the skills route with a personal (no team/org binding) key that failed auth parsing; scripts hitting the proxy without the Authorization header; auth middleware disabled in dev so UserAPIKeyAuth is empty.","solutions":["Authenticate the request with a valid LiteLLM proxy virtual key (Authorization: Bearer sk-...) so the caller has an identity scope","Ensure the key/user is bound to a user, team, or organization in the proxy so owner resolution succeeds","For programmatic calls to the handler, pass user_id explicitly when no UserAPIKeyAuth scope exists"],"exampleFix":"# before\nresp = client.post(\"/v1/skills\", json=skill_payload)  # no auth header\n\n# after\nresp = client.post(\n    \"/v1/skills\",\n    headers={\"Authorization\": \"Bearer sk-my-proxy-key\"},\n    json=skill_payload,\n)","handlingStrategy":"validation","validationCode":"def caller_has_identity(user_api_key_dict, user_id: str | None) -> bool:\n    scope = None\n    if user_api_key_dict is not None:\n        scope = getattr(user_api_key_dict, \"user_id\", None) or getattr(user_api_key_dict, \"team_id\", None) \\\n            or getattr(user_api_key_dict, \"organization_id\", None) or getattr(user_api_key_dict, \"api_key\", None)\n    return bool(scope or user_id)","typeGuard":null,"tryCatchPattern":"try:\n    skill = await LiteLLMSkillsHandler.create_skill(data=payload, user_api_key_dict=auth, user_id=uid)\nexcept ValueError as e:\n    if \"no identity scope\" in str(e):\n        raise HTTPException(status_code=401, detail=\"Authenticated key required to create skills\") from e","preventionTips":["Always send a valid proxy virtual key on skills routes; anonymous access is intentionally rejected","Bind keys to a user/team/org in the proxy so owner attribution always resolves","In tests, construct UserAPIKeyAuth fixtures with at least one identity field instead of empty objects"],"tags":["litellm-proxy","authentication","skills","multi-tenancy"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}