zylon-ai/private-gpt · error · ValueError

Provide SKILL.md either in files or skill_md

Error message

Provide SKILL.md either in files or skill_md

What it means

Raised by a Pydantic model_validator on CreateSkillBody when creating a skill: the request must include SKILL.md content either inline via the skill_md field or as a file entry with path == 'SKILL.md' in the files list. The server rejects the request body before it reaches the skill store. It maps to an HTTP 400 from FastAPI's RequestValidationError handling.

Source

Thrown at private_gpt/server/skills/skill_models.py:74

        description="Instruction loading strategy.",
    )
    readonly: bool = Field(
        default=False, description="Readonly flag for protected skills."
    )
    skill_md: str | None = Field(
        default=None,
        description="Inline SKILL.md content (optional when files includes SKILL.md).",
    )
    files: list[SkillFileInput] = Field(
        default_factory=list,
        description="Optional uploaded files for this skill version.",
    )

    @model_validator(mode="after")
    def validate_skill_md_presence(self) -> "CreateSkillBody":
        has_skill_file = any(file.path == "SKILL.md" for file in self.files)
        if not has_skill_file and not self.skill_md:
            raise ValueError("Provide SKILL.md either in files or skill_md")
        return self


class SkillResponse(BaseModel):
    """Serialized skill object returned by skills endpoints."""

    id: str = Field(description="Unique skill identifier.")
    created_at: datetime = Field(description="Creation timestamp.")
    display_title: str = Field(description="Human display title.")
    latest_version: str | None = Field(
        default=None,
        description="Latest version token for this skill.",
    )
    source: Literal["custom", "anthropic", "zylon"] = Field(
        description="Source of the skill."
    )
    type: Literal["skill"] = Field(default="skill", description="Object type.")
    updated_at: datetime = Field(description="Update timestamp.")

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Add an inline SKILL.md: set skill_md to the full markdown content in the request body.
  2. Or append a file entry with path exactly 'SKILL.md' (exact case) plus its content to files.
  3. If uploading a zip, ensure the archive contains SKILL.md at (or under a wrapper directory containing) its root before sending.
  4. Check the client serializer for path normalization that lowercases or renames entries.

Example fix

// before
{"display_title": "my-skill", "files": [{"path": "README.md", "content": "..."}]}
// after
{"display_title": "my-skill", "skill_md": "---\nname: my-skill\n---\n# My skill", "files": [{"path": "README.md", "content": "..."}]}
Defensive patterns

Strategy: validation

Validate before calling

def has_skill_md(body: dict) -> bool:
    return bool(body.get('skill_md')) or any(
        f.get('path') == 'SKILL.md' for f in body.get('files', [])
    )

assert has_skill_md(payload), 'attach SKILL.md via skill_md or files[]'

Type guard

type SkillPayload = { skill_md?: string; files?: { path: string; content?: string }[] };
const isCreatable = (p: SkillPayload): boolean =>
  Boolean(p.skill_md) || (p.files ?? []).some((f) => f.path === 'SKILL.md');

Try / catch

try {
  await client.post('/skills', payload);
} catch (e: any) {
  if (e?.status === 400 && /SKILL\.md/.test(e.detail)) {
    payload.skill_md = buildDefaultSkillMd();
    await client.post('/skills', payload);
  } else throw e;
}

Prevention

When it happens

Trigger: POST to the skills creation endpoint with neither skill_md set nor any files[] entry whose path is exactly 'SKILL.md' (e.g. {"files": [{"path": "README.md", ...}]} or an entirely empty body). Note the check is case-sensitive: 'skill.md' in files does not satisfy it.

Common situations: Clients uploading only supporting files and forgetting the manifest; typos in the path casing ('Skill.md', 'skill.md'); building the request from a zip whose SKILL.md was flattened or renamed; API version changes that moved SKILL.md from a dedicated field into the files array.

Related errors


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