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
2026-03-31 09:22:21 +07:00
committed by GitHub
parent ffa172c92d
commit 8640503b36
4 changed files with 89 additions and 5 deletions
+8
View File
@@ -2,10 +2,18 @@
const nextConfig = { const nextConfig = {
output: "standalone", output: "standalone",
serverExternalPackages: ["better-sqlite3"], serverExternalPackages: ["better-sqlite3"],
outputFileTracingExcludes: {
"*": [
"**/Cookies/**",
"**/AppData/Local/**",
"**/node_modules/.cache/**",
],
},
images: { images: {
unoptimized: true unoptimized: true
}, },
env: {}, env: {},
turbopack: {},
webpack: (config, { isServer }) => { webpack: (config, { isServer }) => {
// Ignore fs/path modules in browser bundle // Ignore fs/path modules in browser bundle
if (!isServer) { if (!isServer) {
+3 -2
View File
@@ -5,8 +5,8 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --webpack --port 20128", "dev": "next dev --webpack --port 20128",
"build": "NODE_ENV=production next build --webpack", "build": "cross-env NODE_ENV=production next build",
"start": "NODE_ENV=production next start", "start": "cross-env NODE_ENV=production next start",
"dev:bun": "bun --bun next dev --webpack --port 20128", "dev:bun": "bun --bun next dev --webpack --port 20128",
"build:bun": "NODE_ENV=production bun --bun next build --webpack", "build:bun": "NODE_ENV=production bun --bun next build --webpack",
"start:bun": "NODE_ENV=production bun ./.next/standalone/server.js" "start:bun": "NODE_ENV=production bun ./.next/standalone/server.js"
@@ -44,6 +44,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"cross-env": "^10.1.0",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"postcss": "^8.5.6", "postcss": "^8.5.6",
@@ -38,6 +38,7 @@ export default function ProviderDetailPage() {
const [providerStrategy, setProviderStrategy] = useState(null); // null = use global, "round-robin" = override const [providerStrategy, setProviderStrategy] = useState(null); // null = use global, "round-robin" = override
const [providerStickyLimit, setProviderStickyLimit] = useState(""); const [providerStickyLimit, setProviderStickyLimit] = useState("");
const [suggestedModels, setSuggestedModels] = useState([]); const [suggestedModels, setSuggestedModels] = useState([]);
const [kiloFreeModels, setKiloFreeModels] = useState([]);
const { copied, copy } = useCopyToClipboard(); const { copied, copy } = useCopyToClipboard();
const providerInfo = providerNode 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 () => { const fetchConnections = useCallback(async () => {
try { try {
const [connectionsRes, nodesRes, proxyPoolsRes, settingsRes] = await Promise.all([ 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) // Custom models added by user (stored as aliases: modelId → providerAlias/modelId)
const customModels = Object.entries(modelAliases) const customModels = Object.entries(modelAliases)
.filter(([alias, fullModel]) => { .filter(([alias, fullModel]) => {
@@ -556,7 +571,7 @@ export default function ProviderDetailPage() {
return ( return (
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{models.map((model) => { {displayModels.map((model) => {
const fullModel = `${providerStorageAlias}/${model.id}`; const fullModel = `${providerStorageAlias}/${model.id}`;
const oldFormatModel = `${providerId}/${model.id}`; const oldFormatModel = `${providerId}/${model.id}`;
const existingAlias = Object.entries(modelAliases).find( const existingAlias = Object.entries(modelAliases).find(
@@ -575,6 +590,7 @@ export default function ProviderDetailPage() {
testStatus={modelTestResults[model.id]} testStatus={modelTestResults[model.id]}
onTest={connections.length > 0 ? () => handleTestModel(model.id) : undefined} onTest={connections.length > 0 ? () => handleTestModel(model.id) : undefined}
isTesting={testingModelId === model.id} 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" const borderColor = testStatus === "ok"
? "border-green-500/40" ? "border-green-500/40"
: testStatus === "error" : testStatus === "error"
@@ -1040,6 +1056,7 @@ ModelRow.propTypes = {
onCopy: PropTypes.func.isRequired, onCopy: PropTypes.func.isRequired,
testStatus: PropTypes.oneOf(["ok", "error"]), testStatus: PropTypes.oneOf(["ok", "error"]),
isCustom: PropTypes.bool, isCustom: PropTypes.bool,
isFree: PropTypes.bool,
onDeleteAlias: PropTypes.func, onDeleteAlias: PropTypes.func,
onTest: PropTypes.func, onTest: PropTypes.func,
isTesting: PropTypes.bool, isTesting: PropTypes.bool,
@@ -1167,7 +1184,10 @@ function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias
<p className="text-sm font-medium truncate">{modelId}</p> <p className="text-sm font-medium truncate">{modelId}</p>
<div className="flex items-center gap-1 mt-1"> <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"> <div className="relative group/btn">
<button <button
onClick={() => onCopy(fullModel, `model-${modelId}`)} onClick={() => onCopy(fullModel, `model-${modelId}`)}
@@ -0,0 +1,55 @@
import { NextResponse } from "next/server";
const KILO_MODELS_URL = "https://api.kilo.ai/api/gateway/models";
// In-memory cache with TTL
let cachedModels = null;
let cacheTimestamp = 0;
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
export async function GET() {
const now = Date.now();
// Return cached result if still valid
if (cachedModels && now - cacheTimestamp < CACHE_TTL_MS) {
return NextResponse.json({ models: cachedModels, cached: true });
}
try {
const res = await fetch(KILO_MODELS_URL, {
headers: { "Accept": "application/json" },
signal: AbortSignal.timeout(10000),
});
if (!res.ok) {
throw new Error(`Kilo API returned ${res.status}`);
}
const json = await res.json();
const allModels = json.data || [];
const freeModels = allModels
.filter((m) => m.isFree === true)
.map((m) => ({
id: m.id,
name: m.name,
isFree: true,
context_length: m.context_length || 0,
}));
cachedModels = freeModels;
cacheTimestamp = now;
return NextResponse.json({ models: freeModels, cached: false });
} catch (error) {
// Return cached data if available, even if expired
if (cachedModels) {
return NextResponse.json({ models: cachedModels, cached: true, warning: error.message });
}
return NextResponse.json(
{ models: [], error: `Failed to fetch Kilo models: ${error.message}` },
{ status: 502 }
);
}
}