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
- Add an inline SKILL.md: set skill_md to the full markdown content in the request body.
- Or append a file entry with path exactly 'SKILL.md' (exact case) plus its content to files.
- If uploading a zip, ensure the archive contains SKILL.md at (or under a wrapper directory containing) its root before sending.
- 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
- Build skill payloads through a helper that always injects skill_md when files lack SKILL.md.
- Treat SKILL.md path matching as case-sensitive in client code.
- Add a schema test asserting your serializer always emits a SKILL.md source.
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
- 'oneOf' must be an array of schemas
- 'anyOf' must be an array of schemas
- 'allOf' must be an array of schemas
- Invalid page token
- File input requires valid base64 encoded content
AI-assisted analysis of zylon-ai/private-gpt@4a030776a3 (2026-08-15).
Data as JSON: /api/errors/84d5331218557329.
Report an issue: GitHub.