Merge remote-tracking branch 'origin/litellm_/buildkite-litellm-e2e-setup-ff714d' into litellm_/buildkite-litellm-e2e-setup-ff714d

This commit is contained in:
Yuneng Jiang 2026-09-16 16:02:09 -07:00
commit 939757ceff
No known key found for this signature in database
7 changed files with 238 additions and 79 deletions

View file

@ -377,8 +377,7 @@
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_vision": true,
"supports_tool_choice": true,
"cache_read_input_token_cost": 1.5e-08
"supports_tool_choice": true
},
"amazon.nova-2-lite-v1:0": {
"cache_read_input_token_cost": 7.5e-08,
@ -561,8 +560,7 @@
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"cache_read_input_token_cost": 8.75e-09
"supports_tool_choice": true
},
"amazon.nova-pro-v1:0": {
"cache_read_input_token_cost": 2e-07,
@ -578,8 +576,7 @@
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_vision": true,
"supports_tool_choice": true,
"cache_read_input_token_cost": 2e-07
"supports_tool_choice": true
},
"amazon.nova-sonic-v1:0": {
"deprecation_date": "2026-09-14",
@ -45794,8 +45791,7 @@
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_vision": true,
"supports_tool_choice": true,
"cache_read_input_token_cost": 1.5e-08
"supports_tool_choice": true
},
"us.amazon.nova-micro-v1:0": {
"cache_read_input_token_cost": 8.75e-09,
@ -45809,8 +45805,7 @@
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"cache_read_input_token_cost": 8.75e-09
"supports_tool_choice": true
},
"us.amazon.nova-premier-v1:0": {
"deprecation_date": "2026-09-14",
@ -45842,8 +45837,7 @@
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_vision": true,
"supports_tool_choice": true,
"cache_read_input_token_cost": 2e-07
"supports_tool_choice": true
},
"us.anthropic.claude-3-5-haiku-20241022-v1:0": {
"cache_creation_input_token_cost": 1e-06,

View file

@ -377,8 +377,7 @@
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_vision": true,
"supports_tool_choice": true,
"cache_read_input_token_cost": 1.5e-08
"supports_tool_choice": true
},
"amazon.nova-2-lite-v1:0": {
"cache_read_input_token_cost": 7.5e-08,
@ -561,8 +560,7 @@
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"cache_read_input_token_cost": 8.75e-09
"supports_tool_choice": true
},
"amazon.nova-pro-v1:0": {
"cache_read_input_token_cost": 2e-07,
@ -578,8 +576,7 @@
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_vision": true,
"supports_tool_choice": true,
"cache_read_input_token_cost": 2e-07
"supports_tool_choice": true
},
"amazon.nova-sonic-v1:0": {
"deprecation_date": "2026-09-14",
@ -45794,8 +45791,7 @@
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_vision": true,
"supports_tool_choice": true,
"cache_read_input_token_cost": 1.5e-08
"supports_tool_choice": true
},
"us.amazon.nova-micro-v1:0": {
"cache_read_input_token_cost": 8.75e-09,
@ -45809,8 +45805,7 @@
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"cache_read_input_token_cost": 8.75e-09
"supports_tool_choice": true
},
"us.amazon.nova-premier-v1:0": {
"deprecation_date": "2026-09-14",
@ -45842,8 +45837,7 @@
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_vision": true,
"supports_tool_choice": true,
"cache_read_input_token_cost": 2e-07
"supports_tool_choice": true
},
"us.anthropic.claude-3-5-haiku-20241022-v1:0": {
"cache_creation_input_token_cost": 1e-06,

View file

@ -232,7 +232,7 @@ const AddPluginForm: React.FC<AddPluginFormProps> = ({ visible, onClose, accessT
name="skillUrl"
label={labelWithHint(
"Source URL",
"Paste an HTTPS git repository URL from GitHub, GitLab, Bitbucket, or a self-hosted host (e.g. github.com/org/repo or github.com/org/repo/tree/main/my-skill), or an HTTPS link to a .zip archive of the skill hosted on S3 or any static file server.",
"Paste an HTTPS git repository URL from GitHub, GitLab, Bitbucket, or a self-hosted host (e.g. github.com/org/repo or github.com/org/repo/tree/main/my-skill), or an HTTPS link to a .zip archive of the skill hosted on S3 or any static file server. For a private repository use its SSH clone URL (git@ghe.example.com:org/repo.git) so Claude Code clones it with your own SSH key.",
)}
>
{({ ref, onChange, ...field }) => (

View file

@ -156,6 +156,20 @@ describe("getSourceLink", () => {
it("returns null when no repo or url", () => {
expect(getSourceLink({ source: "github" })).toBeNull();
});
it("keeps http and upper-case https urls registered through the api clickable", () => {
expect(getSourceLink({ source: "url", url: "http://git.internal.example/org/repo" })).toBe(
"http://git.internal.example/org/repo",
);
expect(getSourceLink({ source: "git-subdir", url: "HTTPS://gitlab.com/org/repo", path: "sub/dir" })).toBe(
"HTTPS://gitlab.com/org/repo",
);
});
it("returns null for an ssh clone url, which is not browsable", () => {
expect(getSourceLink({ source: "url", url: "git@ghe.example.com:org/repo.git" })).toBeNull();
expect(getSourceLink({ source: "url", url: "ssh://git@ghe.example.com/org/repo.git" })).toBeNull();
});
});
describe("getCategoryBadgeColor", () => {
@ -466,6 +480,70 @@ describe("parseSkillSource", () => {
expect(parseSkillSource("gitlab.com/org/repo", "a//b")).toBeNull();
});
it("keeps an scp-style ssh clone url so private hosts authenticate with the user's key", () => {
expect(parseSkillSource("git@ghe.example.com:org/repo.git")?.parsed).toEqual({
source: "url",
url: "git@ghe.example.com:org/repo.git",
});
expect(parseSkillSource("git@ghe.example.com:org/repo.git")?.suggestedName).toBe("repo");
});
it("stores an ssh clone url exactly as typed, so a forced .git suffix cannot break azure devops or codecommit", () => {
for (const url of [
"git@ghe.example.com:org/repo",
"git@ssh.dev.azure.com:v3/org/project/repo",
"ssh://git@ghe.example.com/org/repo",
"ssh://apka1234@git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo",
"ssh://git@ghe.example.com:2222/org/nested/repo.git",
]) {
expect(parseSkillSource(url)?.parsed).toEqual({ source: "url", url });
}
expect(parseSkillSource("git@ssh.dev.azure.com:v3/org/project/repo")?.suggestedName).toBe("repo");
});
it("accepts an internal host whose last label is not alphabetic, matching the https rule", () => {
expect(parseSkillSource("git@gitlab.internal.k8s2:org/repo.git")?.parsed).toEqual({
source: "url",
url: "git@gitlab.internal.k8s2:org/repo.git",
});
expect(parseSkillSource("https://gitlab.internal.k8s2/org/repo")?.parsed).toEqual({
source: "url",
url: "https://gitlab.internal.k8s2/org/repo",
});
});
it("combines an ssh clone url with an explicit subfolder", () => {
expect(parseSkillSource("git@ghe.example.com:org/repo.git", "plugins/my-skill")?.parsed).toEqual({
source: "git-subdir",
url: "git@ghe.example.com:org/repo.git",
path: "plugins/my-skill",
});
expect(parseSkillSource("git@ghe.example.com:org/repo.git", "../etc")).toBeNull();
});
it("rejects ssh-looking input without a host or repo path", () => {
expect(parseSkillSource("git@ghe.example.com:repo.git")).toBeNull();
expect(parseSkillSource("git@localhost:org/repo.git")).toBeNull();
expect(parseSkillSource("git@:org/repo.git")).toBeNull();
expect(parseSkillSource("ssh://ghe.example.com/org/repo.git")).toBeNull();
});
it("rejects ssh remotes with ip hosts or traversal segments", () => {
expect(parseSkillSource("git@10.0.0.5:org/repo.git")).toBeNull();
expect(parseSkillSource("ssh://git@169.254.169.254/org/repo")).toBeNull();
expect(parseSkillSource("git@ghe.example.com:../etc")).toBeNull();
expect(parseSkillSource("ssh://git@ghe.example.com/org/../repo")).toBeNull();
expect(parseSkillSource("git@ghe.example.com:org/../../etc/passwd")).toBeNull();
expect(parseSkillSource("git@ghe.example.com:org/.github")?.parsed).toEqual({
source: "url",
url: "git@ghe.example.com:org/.github",
});
});
it("rejects an ssh remote carrying a password, which would publish a secret on the feed", () => {
expect(parseSkillSource("ssh://git:s3cret@ghe.example.com/org/repo.git")).toBeNull();
});
it("returns null for empty and garbage input", () => {
expect(parseSkillSource("")).toBeNull();
expect(parseSkillSource(" ")).toBeNull();
@ -568,7 +646,7 @@ describe("parseSkillSource", () => {
// Skill sources are served on the unauthenticated public feeds and cloned by clients, so the
// parser must never publish an insecure, credentialed, internal, or malformed clone URL.
describe("parseSkillSource — security boundary", () => {
it("rejects non-https schemes", () => {
it("rejects schemes other than https and user-qualified ssh", () => {
for (const url of [
"http://gitlab.com/org/repo",
"HTTP://gitlab.com/org/repo",

View file

@ -29,17 +29,34 @@ export const SHA256_REGEX = /^[0-9a-fA-F]{64}$/;
export const isValidSha256 = (digest: string): boolean => digest.trim() === "" || SHA256_REGEX.test(digest.trim());
// WHATWG normalizes obfuscated IPv4 (e.g. 2130706433, 0x7f.0.0.1) to dotted-decimal, so this
// catches every IPv4 form; bracketed IPv6 is rejected separately.
// WHATWG normalizes obfuscated IPv4 (e.g. 2130706433, 0x7f.0.0.1) to dotted-decimal on https, so
// this catches every IPv4 form there; on a non-special scheme like ssh it catches the dotted form
// only. Bracketed IPv6 is rejected separately.
const IPV4_HOST_REGEX = /^\d{1,3}(\.\d{1,3}){3}$/;
const GITHUB_ORG_REGEX = /^[A-Za-z0-9-]+$/;
const GITHUB_REPO_REGEX = /^[A-Za-z0-9._-]+$/;
const BROWSABLE_URL_REGEX = /^https?:\/\//i;
const SSH_SCHEME = "ssh://";
const SSH_SCP_REGEX = /^([a-z0-9._-]+)@([^:/@]+):(?!\/)(.+)$/i;
const buildRepoUrl = (url: URL): string => `${url.protocol}//${url.host}${url.pathname.replace(/\/+$/, "")}`;
const pathSegments = (url: URL): string[] => url.pathname.split("/").filter((seg) => seg !== "");
const toUrl = (candidate: string): URL | null => {
try {
return new URL(candidate);
} catch {
return null;
}
};
/** One host rule for every scheme, so an ssh remote is neither more nor less trusted than its https twin. */
const isSafeHost = (url: URL): boolean =>
url.hostname.includes(".") && !url.hostname.startsWith("[") && !IPV4_HOST_REGEX.test(url.hostname);
/**
* Validate and normalize a repository URL into a parsed URL, or null. Enforces https (rejects
* http/ssh/git/etc.), rejects embedded credentials, and requires a dotted host, so the public
@ -52,20 +69,8 @@ const parseRepoUrl = (raw: string): URL | null => {
return null;
}
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
let url: URL;
try {
url = new URL(withScheme);
} catch {
return null;
}
if (
url.protocol !== "https:" ||
url.username !== "" ||
url.password !== "" ||
!url.hostname.includes(".") ||
url.hostname.startsWith("[") ||
IPV4_HOST_REGEX.test(url.hostname)
) {
const url = toUrl(withScheme);
if (!url || url.protocol !== "https:" || url.username !== "" || url.password !== "" || !isSafeHost(url)) {
return null;
}
return url;
@ -140,13 +145,12 @@ const parseGitHubSource = (url: URL, subPath?: string): SkillSourcePreview | nul
return repoPreview;
};
const parseRawGitSource = (url: URL, subPath?: string): SkillSourcePreview | null => {
if (pathSegments(url).length < 2) {
return null;
}
const repoUrl = buildRepoUrl(url);
const buildGitSourcePreview = (
kind: "Git" | "SSH",
repoUrl: string,
repoName: string,
subPath?: string,
): SkillSourcePreview | null => {
const normalized = normalizeSubPath(subPath ?? "");
if (normalized !== "") {
if (!SUBDIR_PATH_REGEX.test(normalized)) {
@ -154,18 +158,51 @@ const parseRawGitSource = (url: URL, subPath?: string): SkillSourcePreview | nul
}
return {
parsed: { source: "git-subdir", url: repoUrl, path: normalized },
label: `Git subdir — ${repoUrl} @ ${normalized}`,
label: `${kind} subdir — ${repoUrl} @ ${normalized}`,
suggestedName: toKebabCase(lastSegment(normalized)),
};
}
return {
parsed: { source: "url", url: repoUrl },
label: `Git repo — ${repoUrl}`,
suggestedName: toKebabCase(lastSegment(url.pathname).replace(/\.git$/, "")),
label: `${kind} repo — ${repoUrl}`,
suggestedName: toKebabCase(repoName),
};
};
const parseRawGitSource = (url: URL, subPath?: string): SkillSourcePreview | null => {
if (pathSegments(url).length < 2) {
return null;
}
const repoName = lastSegment(url.pathname).replace(/\.git$/, "");
return buildGitSourcePreview("Git", buildRepoUrl(url), repoName, subPath);
};
/**
* Parse an scp-style `git@host:org/repo` or `ssh://git@host/org/repo` clone URL, registering it
* exactly as typed: git treats the `.git` suffix as optional, and forcing one on breaks hosts whose
* paths are not `org/repo`, like Azure DevOps `v3/...` and CodeCommit `v1/repos/...`. The scp form is
* rewritten to `ssh://` only to reuse the https host and credential rules, and only a URL that
* survives that round trip unchanged is accepted, which keeps traversal segments off the feed.
*/
const parseSshSource = (raw: string, subPath?: string): SkillSourcePreview | null => {
const trimmed = raw.trim();
const scp = SSH_SCP_REGEX.exec(trimmed);
const candidate = scp ? `${SSH_SCHEME}${scp[1]}@${scp[2]}/${scp[3]}` : trimmed;
if (!candidate.toLowerCase().startsWith(SSH_SCHEME)) {
return null;
}
const url = toUrl(candidate);
if (!url || url.username === "" || url.password !== "" || !isSafeHost(url)) {
return null;
}
const pathStart = candidate.indexOf("/", SSH_SCHEME.length);
if (pathStart === -1 || url.pathname !== candidate.slice(pathStart) || pathSegments(url).length < 2) {
return null;
}
return buildGitSourcePreview("SSH", trimmed, lastSegment(url.pathname).replace(/\.git$/i, ""), subPath);
};
const parseArchiveSource = (url: URL): SkillSourcePreview => ({
parsed: { source: "archive", url: url.href },
label: `Zip archive — ${url.host}${url.pathname}`,
@ -175,10 +212,15 @@ const parseArchiveSource = (url: URL): SkillSourcePreview => ({
/**
* Parse any git-accessible repository URL or https zip archive URL into a registerable skill
* source. A `.zip` path is an `archive` source (S3, Artifactory, any static host). GitHub URLs
* keep their `github`/`git-subdir` shorthand; every other host is treated as a raw repo URL,
* keep their `github`/`git-subdir` shorthand; ssh clone URLs stay ssh so a private host
* authenticates with the user's own key; every other host is treated as a raw repo URL,
* with an optional subfolder turning it into git-subdir.
*/
export const parseSkillSource = (rawUrl: string, subPath?: string): SkillSourcePreview | null => {
const ssh = parseSshSource(rawUrl, subPath);
if (ssh) {
return ssh;
}
const url = parseRepoUrl(rawUrl);
if (!url) {
return null;
@ -268,14 +310,14 @@ export const getSourceDisplayText = (source: PluginSource): string => {
};
/**
* Get clickable link for plugin source
* Get clickable link for plugin source. Ssh clone urls are not browsable, so they yield null.
*/
export const getSourceLink = (source: PluginSource): string | null => {
if (source.source === "github" && source.repo) {
return `https://github.com/${source.repo}`;
}
const linksToUrl = source.source === "url" || source.source === "git-subdir" || source.source === "archive";
return linksToUrl && source.url ? source.url : null;
return linksToUrl && source.url && BROWSABLE_URL_REGEX.test(source.url) ? source.url : null;
};
/**

View file

@ -0,0 +1,42 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { Plugin } from "./types";
import SkillDetail from "./skill_detail";
const buildSkill = (source: Plugin["source"]): Plugin => ({
id: "plugin-id",
name: "my-skill",
source,
enabled: true,
});
describe("SkillDetail source", () => {
it("links a github source to the repository", () => {
render(<SkillDetail skill={buildSkill({ source: "github", repo: "org/repo" })} onBack={vi.fn()} />);
expect(screen.getByRole("link", { name: "github.com/org/repo" })).toHaveAttribute(
"href",
"https://github.com/org/repo",
);
});
it("renders an ssh clone url as plain text instead of an unusable link", () => {
render(
<SkillDetail skill={buildSkill({ source: "url", url: "git@ghe.example.com:org/repo.git" })} onBack={vi.fn()} />,
);
expect(screen.getByText("git@ghe.example.com:org/repo.git")).toBeInTheDocument();
expect(screen.queryByRole("link")).not.toBeInTheDocument();
});
it("renders an ssh git-subdir source as plain text without a tree path", () => {
render(
<SkillDetail
skill={buildSkill({ source: "git-subdir", url: "git@ghe.example.com:org/repo.git", path: "plugins/x" })}
onBack={vi.fn()}
/>,
);
expect(screen.getByText("git@ghe.example.com:org/repo.git @ plugins/x")).toBeInTheDocument();
expect(screen.queryByRole("link")).not.toBeInTheDocument();
});
});

View file

@ -1,8 +1,38 @@
import React, { useState } from "react";
import { ArrowLeft, Check, Copy, Link2 } from "lucide-react";
import { cn } from "@/lib/cva.config";
import { buildMarketplaceSettingsSnippet, formatInstallCommand } from "./helpers";
import { Plugin } from "./types";
import { buildMarketplaceSettingsSnippet, formatInstallCommand, getSourceDisplayText, getSourceLink } from "./helpers";
import { Plugin, PluginSource } from "./types";
const SkillSource: React.FC<{ source: PluginSource }> = ({ source }) => {
const link = getSourceLink(source);
const href = link && source.source === "git-subdir" && source.path ? `${link}/tree/main/${source.path}` : link;
if (href) {
return (
<div className="mb-6">
<div className="mb-1 text-xs text-muted-foreground">Source</div>
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 break-all text-[13px] text-info"
>
{href.replace("https://", "")}
<Link2 className="size-3 shrink-0" />
</a>
</div>
);
}
if (!source.url) {
return null;
}
return (
<div className="mb-6">
<div className="mb-1 text-xs text-muted-foreground">Source</div>
<div className="break-all text-[13px] text-foreground">{getSourceDisplayText(source)}</div>
</div>
);
};
interface SkillDetailProps {
skill: Plugin;
@ -22,14 +52,6 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
setTimeout(() => setCopiedKey(null), 2000);
};
const sourceUrl = (() => {
const src = skill.source;
if (src.source === "github" && src.repo) return `https://github.com/${src.repo}`;
if (src.source === "git-subdir" && src.url) return src.path ? `${src.url}/tree/main/${src.path}` : src.url;
if ((src.source === "url" || src.source === "archive") && src.url) return src.url;
return null;
})();
const installCommand = formatInstallCommand(skill);
const settingsSnippet = buildMarketplaceSettingsSnippet(
@ -128,20 +150,7 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
</span>
</div>
{sourceUrl && (
<div className="mb-6">
<div className="mb-1 text-xs text-muted-foreground">Source</div>
<a
href={sourceUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 break-all text-[13px] text-info"
>
{sourceUrl.replace("https://", "")}
<Link2 className="size-3 shrink-0" />
</a>
</div>
)}
<SkillSource source={skill.source} />
{skill.keywords && skill.keywords.length > 0 && (
<div className="mb-6">