feat(kilo): fetch free models from Kilo API + Windows build fixes (#455)

- Add /api/providers/kilo/free-models endpoint with 1hr cache
- Fetch and merge Kilo free models with hardcoded models for kilocode provider
- Display 'Free' badge on models fetched from Kilo API
- Fix Windows build: add cross-env, remove --webpack flag, add turbopack config
- Add outputFileTracingExcludes for Windows system directories
This commit is contained in:
Vishal Raj V authored and GitHub committed 2026-03-31 09:22:21 +07:00
1 parent ffa172c92d
commit 8640503b36
4 files changed
+89 -5

No files matched your search

@@ -38,6 +38,7 @@ export default function ProviderDetailPage() {
const [providerStrategy, setProviderStrategy] = useState(null); // null = use global, "round-robin" = override
const [providerStickyLimit, setProviderStickyLimit] = useState("");
const [suggestedModels, setSuggestedModels] = useState([]);
const [kiloFreeModels, setKiloFreeModels] = useState([]);
const { copied, copy } = useCopyToClipboard();
const providerInfo = providerNode
@@ -77,6 +78,15 @@ export default function ProviderDetailPage() {
}
}, []);
// Fetch free models from Kilo API for kilocode provider
useEffect(() => {
if (providerId !== "kilocode") return;
fetch("/api/providers/kilo/free-models")
.then((res) => res.json())
.then((data) => { if (data.models?.length) setKiloFreeModels(data.models); })
.catch(() => {});
}, [providerId]);
const fetchConnections = useCallback(async () => {
try {
const [connectionsRes, nodesRes, proxyPoolsRes, settingsRes] = await Promise.all([
@@ -537,6 +547,11 @@ export default function ProviderDetailPage() {
/>
);
}
// Combine hardcoded models with Kilo free models (deduplicated)
const displayModels = [
...models,
...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)),
];
// Custom models added by user (stored as aliases: modelId → providerAlias/modelId)
const customModels = Object.entries(modelAliases)
.filter(([alias, fullModel]) => {
@@ -556,7 +571,7 @@ export default function ProviderDetailPage() {
return (
<div className="flex flex-wrap gap-3">
{models.map((model) => {
{displayModels.map((model) => {
const fullModel = `${providerStorageAlias}/${model.id}`;
const oldFormatModel = `${providerId}/${model.id}`;
const existingAlias = Object.entries(modelAliases).find(
@@ -575,6 +590,7 @@ export default function ProviderDetailPage() {
testStatus={modelTestResults[model.id]}
onTest={connections.length > 0 ? () => handleTestModel(model.id) : undefined}
isTesting={testingModelId === model.id}
isFree={model.isFree}
/>
);
})}
@@ -964,7 +980,7 @@ export default function ProviderDetailPage() {
);
}
function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, onDeleteAlias, onTest, isTesting }) {
function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, isFree, onDeleteAlias, onTest, isTesting }) {
const borderColor = testStatus === "ok"
? "border-green-500/40"
: testStatus === "error"
@@ -1040,6 +1056,7 @@ ModelRow.propTypes = {
onCopy: PropTypes.func.isRequired,
testStatus: PropTypes.oneOf(["ok", "error"]),
isCustom: PropTypes.bool,
isFree: PropTypes.bool,
onDeleteAlias: PropTypes.func,
onTest: PropTypes.func,
isTesting: PropTypes.bool,
@@ -1167,7 +1184,10 @@ function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias
<p className="text-sm font-medium truncate">{modelId}</p>
<div className="flex items-center gap-1 mt-1">
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code>
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code>
{isFree && (
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Free</span>
)}
<div className="relative group/btn">
<button
onClick={() => onCopy(fullModel, `model-${modelId}`)}