BerriAI/litellm · error · HTTPException

source.source must be 'github', 'url', or 'git-subdir'

Error message

source.source must be 'github', 'url', or 'git-subdir'

What it means

400 thrown by _validate_plugin_source() when the source object's "source" discriminator is anything other than the three supported literals: 'github', 'url', or 'git-subdir'. The match must be exact — the check is a string equality chain, so near-misses fall through to this else-branch.

Source

Thrown at litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py:202

                status_code=400,
                detail={
                    "error": "git-subdir source must include 'url' field (e.g., 'https://github.com/org/repo.git')"
                },
            )
        if not source.get("path"):
            raise HTTPException(
                status_code=400,
                detail={"error": "git-subdir source must include 'path' field (e.g., 'plugins/plugin-name')"},
            )
        if not _VALID_GIT_SUBDIR_PATH_RE.match(source["path"]):
            raise HTTPException(
                status_code=400,
                detail={
                    "error": "git-subdir 'path' must be a relative path of the form 'segment/segment' (alphanumeric, dots, hyphens, underscores only)"
                },
            )
    else:
        raise HTTPException(
            status_code=400,
            detail={"error": "source.source must be 'github', 'url', or 'git-subdir'"},
        )


def _build_plugin_manifest(name: str, spec: PluginSpec) -> Mapping[str, object]:
    """Build the stored manifest dict shared by plugin create and update."""
    dumped: Final[Mapping[str, object]] = spec.model_dump(exclude_none=True)
    return {"name": name, **{key: value for key, value in dumped.items() if value and key != "name"}}


def _error_response(status_code: int, message: str) -> HTTPException:
    return HTTPException(status_code=status_code, detail={"error": message})


def _name_conflict_error(name: str) -> HTTPException:
    return _error_response(
        409, f"A skill named '{name}' already exists. Update the existing skill instead of adding it again."

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Set "source" to exactly one of: "github" (with "repo"), "url" (with "url"), or "git-subdir" (with "url" and "path").
  2. Check for underscores, capitals, or truncation in the type string — 'git-subdir' is the only hyphenated form accepted.
  3. Confirm the 'source' key exists at all inside the source object; omitting it produces this same error.

Example fix

# before
{"source": {"source": "git_subdir", "url": "https://github.com/org/repo.git", "path": "plugins/p"}}

# after
{"source": {"source": "git-subdir", "url": "https://github.com/org/repo.git", "path": "plugins/p"}}
Defensive patterns

Strategy: validation

Validate before calling

ALLOWED = {"github", "url", "git-subdir"}
stype = payload["source"].get("source")
if stype not in ALLOWED:
    raise ValueError(f"source.source must be one of {sorted(ALLOWED)}, got {stype!r}")

Type guard

const SOURCE_TYPES = ['github', 'url', 'git-subdir'] as const;
type SourceType = typeof SOURCE_TYPES[number];
function isSourceType(v: unknown): v is SourceType {
  return typeof v === 'string' && (SOURCE_TYPES as readonly string[]).includes(v);
}

Prevention

When it happens

Trigger: POST /claude-code/plugins or PUT with "source": {"source": "git"}, "git_subdir" (underscore), "Github" (capitalized), "gitrepo", or a null/missing source key — all hit the else branch and return this 400.

Common situations: Typo or underscore variant of the type tag; capitalization differences from copy-pasting docs headings; a missing 'source' key entirely (source.get('source') returns None); schema drift between what your tooling emits and the three accepted literals.

Related errors


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