{"record":{"id":"604c35d949d01af0","repo":"BerriAI/litellm","slug":"source-source-must-be-github-url-or-git-sub","errorCode":null,"errorMessage":"source.source must be 'github', 'url', or 'git-subdir'","messagePattern":"source\\.source must be 'github', 'url', or 'git-subdir'","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py","lineNumber":202,"sourceCode":"                status_code=400,\n                detail={\n                    \"error\": \"git-subdir source must include 'url' field (e.g., 'https://github.com/org/repo.git')\"\n                },\n            )\n        if not source.get(\"path\"):\n            raise HTTPException(\n                status_code=400,\n                detail={\"error\": \"git-subdir source must include 'path' field (e.g., 'plugins/plugin-name')\"},\n            )\n        if not _VALID_GIT_SUBDIR_PATH_RE.match(source[\"path\"]):\n            raise HTTPException(\n                status_code=400,\n                detail={\n                    \"error\": \"git-subdir 'path' must be a relative path of the form 'segment/segment' (alphanumeric, dots, hyphens, underscores only)\"\n                },\n            )\n    else:\n        raise HTTPException(\n            status_code=400,\n            detail={\"error\": \"source.source must be 'github', 'url', or 'git-subdir'\"},\n        )\n\n\ndef _build_plugin_manifest(name: str, spec: PluginSpec) -> Mapping[str, object]:\n    \"\"\"Build the stored manifest dict shared by plugin create and update.\"\"\"\n    dumped: Final[Mapping[str, object]] = spec.model_dump(exclude_none=True)\n    return {\"name\": name, **{key: value for key, value in dumped.items() if value and key != \"name\"}}\n\n\ndef _error_response(status_code: int, message: str) -> HTTPException:\n    return HTTPException(status_code=status_code, detail={\"error\": message})\n\n\ndef _name_conflict_error(name: str) -> HTTPException:\n    return _error_response(\n        409, f\"A skill named '{name}' already exists. Update the existing skill instead of adding it again.\"","sourceCodeStart":184,"sourceCodeEnd":220,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py#L184-L220","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Set \"source\" to exactly one of: \"github\" (with \"repo\"), \"url\" (with \"url\"), or \"git-subdir\" (with \"url\" and \"path\").","Check for underscores, capitals, or truncation in the type string — 'git-subdir' is the only hyphenated form accepted.","Confirm the 'source' key exists at all inside the source object; omitting it produces this same error."],"exampleFix":"# before\n{\"source\": {\"source\": \"git_subdir\", \"url\": \"https://github.com/org/repo.git\", \"path\": \"plugins/p\"}}\n\n# after\n{\"source\": {\"source\": \"git-subdir\", \"url\": \"https://github.com/org/repo.git\", \"path\": \"plugins/p\"}}","handlingStrategy":"validation","validationCode":"ALLOWED = {\"github\", \"url\", \"git-subdir\"}\nstype = payload[\"source\"].get(\"source\")\nif stype not in ALLOWED:\n    raise ValueError(f\"source.source must be one of {sorted(ALLOWED)}, got {stype!r}\")","typeGuard":"const SOURCE_TYPES = ['github', 'url', 'git-subdir'] as const;\ntype SourceType = typeof SOURCE_TYPES[number];\nfunction isSourceType(v: unknown): v is SourceType {\n  return typeof v === 'string' && (SOURCE_TYPES as readonly string[]).includes(v);\n}","tryCatchPattern":null,"preventionTips":["Encode the three accepted literals as an enum/const in client code so typos cannot compile.","Watch for underscore variants ('git_subdir') and capitalization when porting examples."],"tags":["litellm-proxy","claude-code-marketplace","validation","http-400","plugin-registration","enum"],"backgroundTag":"request-validation-failed","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}