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
- Call through an authenticated proxy route so a populated UserAPIKeyAuth is available
- Or pass the request's user_api_key_dict explicitly: await LiteLLMSkillsHandler.create_skill(data, user_api_key_dict=auth)
- Or pass an explicit user_id when no auth dict exists
- 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
- Only expose skill creation through authenticated proxy routes so UserAPIKeyAuth is always populated
- Always pass the request's auth dict (or an explicit user_id) when calling the handler programmatically
- Never rely on placeholder owners; the guard exists to prevent cross-tenant skill leakage
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
- Skill not found
- Admin-only endpoint. Not allowed to access this., your role=
- f"key not allowed to access this user's info. user_id=
- Gemini managed-agent endpoints require a caller-supplied…
- Key not found
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 Base64View on GitHub (pinned to 77b7c6c40c)