zylon-ai/private-gpt · error · ValueError

Unknown message role {self.role}. Expected 'system', 'user',

Error message

Unknown message role {self.role}. Expected 'system', 'user', 'assistant', or 'tool'.

What it means

Raised by POST /skills/{skill_id}/versions when the target skill does not exist in the specified collection. Version creation is only valid for an existing skill, so the pre-check on get_skill returning None yields 404.

Source

Thrown at private_gpt/chat/input_models.py:875

                current_group.append(msg)

        if current_group:
            groups.append(current_group)

        return groups

    def _convert_into_llama_index_messages(
        self,
        tool_uses: dict[str, ToolUseBlock] | None = None,
    ) -> tuple[list["ChatMessage"], dict[str, ToolUseBlock]]:
        tool_uses = tool_uses or {}

        if self.role in ["system", "user"]:
            return self._convert_message(), tool_uses
        elif self.role == "assistant":
            return self._convert_assistant_message(tool_uses)
        else:
            raise ValueError(
                f"Unknown message role {self.role}. Expected 'system', 'user', 'assistant', or 'tool'."
            )

    def _extract_content(
        self,
        content: str | BaseContentBlock | Sequence[ContentBlockType] | None,
    ) -> tuple[list[LIContentBlock] | None, dict[str, list[ContentBlockType]] | None]:
        """Extract text content from various content types."""
        blocks: list[LIContentBlock] = []
        custom_blocks: dict[str, list[ContentBlockType]] = {}
        if isinstance(content, str):
            blocks.append(LITextBlock(text=content))
            content_blocks: Sequence[ContentBlockType] = []
        elif isinstance(content, BaseContentBlock):
            content_blocks = [cast(ContentBlockType, content)]
        else:
            content_blocks = content or []

View on GitHub (pinned to 4a030776a3)

Solutions

  1. GET the skill first to confirm it exists in the collection, then post the version.
  2. If 404, recreate the skill (POST /skills) instead of adding a version.
  3. Refresh skill state in the editor before the version upload.

Example fix

// before
await skillsApi.createVersion(id, collection, files);

// after
const existing = await skillsApi.get(id, collection).catch(() => null);
if (!existing) { await skillsApi.create(body); } // recreate instead
else { await skillsApi.createVersion(id, collection, files); }
Defensive patterns

Strategy: validation

Validate before calling

const skill = await skillsApi.get(id, collection).catch(() => null);
if (!skill) { await skillsApi.create({...body}); /* create instead of version */ }

Type guard

const canVersion = (s) => s != null && !s.readonly;

Try / catch

try { await skillsApi.createVersion(id, collection, files); }
catch (e) {
  if (e.status === 404) { await skillsApi.create({...skillBody, files}); } // recreate
  else throw e;
}

Prevention

When it happens

Trigger: Posting a new version for a skill id that was deleted, belongs to another collection, or is misspelled; racing with a concurrent delete.

Common situations: Uploading a new version from a stale edit screen after the skill was removed elsewhere; collection switch in the UI not propagated to the version form.

Related errors


AI-assisted analysis of zylon-ai/private-gpt@4a030776a3 (2026-08-15). Data as JSON: /api/errors/4713fecc79c29de9. Report an issue: GitHub.