BerriAI/litellm · error · ValueError

Unable to record skill ownership: caller has no identity…

Error message

Unable to record skill ownership: caller has no identity scope.

What it means

When creating a skill, the handler derives the owner from the authenticated caller (get_primary_resource_owner_scope on the UserAPIKeyAuth) with user_id as fallback. If neither yields an identity, it refuses with ValueError instead of stamping a placeholder: a NULL or shared owner would let any identity-less caller read everyone's skills, so creation is rejected outright.

Solutions

  1. Call through an authenticated proxy route so a populated UserAPIKeyAuth is available
  2. Or pass the request's user_api_key_dict explicitly: await LiteLLMSkillsHandler.create_skill(data, user_api_key_dict=auth)
  3. Or pass an explicit user_id when no auth dict exists
  4. Ensure the virtual key/user carries a usable scope (user, team, org, or key id)

Example fix

# before
skill = await LiteLLMSkillsHandler.create_skill(data)  # ValueError: caller has no identity scope

# after
skill = await LiteLLMSkillsHandler.create_skill(
    data,
    user_api_key_dict=user_api_key_dict,  # from the authenticated request
)
# or, for trusted internal callers:
skill = await LiteLLMSkillsHandler.create_skill(data, user_id="user-123")
Defensive patterns

Strategy: validation

Validate before calling

from litellm.proxy.auth.auth_checks import get_primary_resource_owner_scope

def has_identity_scope(user_api_key_dict, user_id: str | None) -> bool:
    return bool(get_primary_resource_owner_scope(user_api_key_dict) or user_id)

if not has_identity_scope(auth_dict, user_id):
    raise HTTPException(status_code=401, detail="Authenticated caller required to create skills")

Try / catch

try:
    skill = await LiteLLMSkillsHandler.create_skill(data, user_api_key_dict=auth, user_id=user_id)
except ValueError as e:
    if "no identity scope" in str(e):
        raise HTTPException(status_code=401, detail="Skill creation requires an authenticated caller") from e
    raise

Prevention

When it happens

Trigger: create_skill invoked with user_api_key_dict=None (or a dict lacking any user/team/org/api_key scope) and user_id=None — typically internal scripts, background jobs, or tests calling the handler directly instead of through an authenticated proxy route.

Common situations: Custom routes that bypass the proxy's auth middleware; test fixtures constructing NewSkillRequest without an auth context; refactoring code that previously relied on implicit identity.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/81c4606f7912dbb3. Report an issue: GitHub.

Appendix: source

Thrown at litellm/llms/litellm_proxy/skills/handler.py:77

        return prisma_client

    @staticmethod
    async def create_skill(
        data: NewSkillRequest,
        user_id: str | None = None,
        user_api_key_dict: UserAPIKeyAuth | None = None,
    ) -> LiteLLM_SkillsTable:
        prisma_client: Final = await LiteLLMSkillsHandler._get_prisma_client()

        skill_id: Final = f"{LITELLM_SKILL_ID_PREFIX}{uuid.uuid4()}"
        owner: Final = get_primary_resource_owner_scope(user_api_key_dict) or user_id
        if owner is None:
            # Identity-less callers (no user_id / team_id / org_id /
            # api_key / token) can't be uniquely stamped on the row.
            # Stamping a placeholder would let any two such callers see
            # each other's skills via the shared owner. ValueError keeps
            # this module FastAPI-free per the project layering rule.
            raise ValueError("Unable to record skill ownership: caller has no identity scope.")

        skill_data: Final[dict[str, Any]] = {
            "skill_id": skill_id,
            "display_title": data.display_title,
            "description": data.description,
            "instructions": data.instructions,
            "source": "custom",
            "created_by": owner,
            "updated_by": owner,
        }

        if data.metadata is not None:
            from litellm.litellm_core_utils.safe_json_dumps import safe_dumps

            skill_data["metadata"] = safe_dumps(data.metadata)

        if data.file_content is not None:
            from prisma.fields import Base64

View on GitHub (pinned to 77b7c6c40c)